Bug 1425837 - Part 3: Don't store AnimationArray (a.k.a. AnimationInfo::mAnimations) on the compositor thread. r=birtles
authorBoris Chiou <boris.chiou@gmail.com>
Mon, 18 Mar 2019 18:04:48 +0000
changeset 464936 6c55b82deb9ba9b5924ef6bfdecdffaf9f42307a
parent 464935 5d416b07c9b9886d7456e9e209ed93247e497af8
child 464937 b27d18cccc623d49dfcd89aa07890ab940d859f5
push id80792
push userbchiou@mozilla.com
push dateTue, 19 Mar 2019 08:04:11 +0000
treeherderautoland@08126c1f53fe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbirtles
bugs1425837
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1425837 - Part 3: Don't store AnimationArray (a.k.a. AnimationInfo::mAnimations) on the compositor thread. r=birtles The original implementation about "setting animations" is a little bit hard to read. In `SetAnimations()`, we create a new intermediate data, `AnimData`, and we mutate the original animations. And then iterate this mutated animations & intermediate data for sampling. In this bug, we are planning to group the AnimData as a useful data structure for supporting multiple properties transform-like animations, so it seems the structure of original animations may be hard to use after that. Therefore, we decide to do some reworks on this: First, we do renames, 1. InfalliableTArray to nsTArray. (They are the same.) 2. AnimData to PropertyAnimation. 3. SetAnimations() to ExtractAnimations(), which returns nsTArray<PropertyAnimationGroup>. Each entry in the array is for one property. In this patch, there is only one entry. We will extend this to multiple entries in the next patch. And then rework `ExtractAnimations()`, which stores all the necessary data in `PropertyAnimationGroup`. For WR, we store this in `CompositorAnimationStorage`. For non-WR, we store it in `AnimationInfo`. So we can just use this organized data structure for supporting multiple properties animations. (See the next patch.) Depends on D22563 Differential Revision: https://phabricator.services.mozilla.com/D23062
gfx/layers/AnimationHelper.cpp
gfx/layers/AnimationHelper.h
gfx/layers/AnimationInfo.cpp
gfx/layers/AnimationInfo.h
gfx/layers/Layers.cpp
gfx/layers/Layers.h
gfx/layers/composite/AsyncCompositionManager.cpp
gfx/layers/ipc/CompositorBridgeParent.cpp
gfx/layers/ipc/LayerTransactionParent.cpp
--- a/gfx/layers/AnimationHelper.cpp
+++ b/gfx/layers/AnimationHelper.cpp
@@ -121,80 +121,85 @@ void CompositorAnimationStorage::SetAnim
   auto count = mAnimatedValues.Count();
   AnimatedValue* value = mAnimatedValues.LookupOrAdd(aId, aOpacity);
   if (count == mAnimatedValues.Count()) {
     MOZ_ASSERT(value->mType == AnimatedValue::OPACITY);
     value->mOpacity = aOpacity;
   }
 }
 
-AnimationArray* CompositorAnimationStorage::GetAnimations(
+nsTArray<PropertyAnimationGroup>* CompositorAnimationStorage::GetAnimations(
     const uint64_t& aId) const {
   MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
   return mAnimations.Get(aId);
 }
 
 void CompositorAnimationStorage::SetAnimations(uint64_t aId,
                                                const AnimationArray& aValue) {
   MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
-  AnimationArray* value = new AnimationArray(aValue);
-  mAnimations.Put(aId, value);
+  mAnimations.Put(aId, new nsTArray<PropertyAnimationGroup>(
+                           AnimationHelper::ExtractAnimations(aValue)));
 }
 
 AnimationHelper::SampleResult AnimationHelper::SampleAnimationForEachNode(
     TimeStamp aPreviousFrameTime, TimeStamp aCurrentFrameTime,
-    AnimationArray& aAnimations, InfallibleTArray<AnimData>& aAnimationData,
+    nsTArray<PropertyAnimationGroup>& aPropertyAnimationGroups,
     RefPtr<RawServoAnimationValue>& aAnimationValue,
     const AnimatedValue* aPreviousValue) {
-  MOZ_ASSERT(!aAnimations.IsEmpty(), "Should be called with animations");
+  MOZ_ASSERT(!aPropertyAnimationGroups.IsEmpty(),
+             "Should be called with animations");
+
+  // FIXME: Will extend this list to multiple properties. For now, its length is
+  // always 1.
+  MOZ_ASSERT(aPropertyAnimationGroups.Length() == 1);
+  PropertyAnimationGroup& propertyAnimationGroup = aPropertyAnimationGroups[0];
 
   bool hasInEffectAnimations = false;
 #ifdef DEBUG
   // In cases where this function returns a SampleResult::Skipped, we actually
   // do populate aAnimationValue in debug mode, so that we can MOZ_ASSERT at the
   // call site that the value that would have been computed matches the stored
   // value that we end up using. This flag is used to ensure we populate
   // aAnimationValue in this scenario.
   bool shouldBeSkipped = false;
 #endif
-  // Process in order, since later aAnimations override earlier ones.
-  for (size_t i = 0, iEnd = aAnimations.Length(); i < iEnd; ++i) {
-    Animation& animation = aAnimations[i];
-    AnimData& animData = aAnimationData[i];
-
+  bool isSingleAnimation = propertyAnimationGroup.mAnimations.Length() == 1;
+  // Initialize by using base style.
+  aAnimationValue = propertyAnimationGroup.mBaseStyle;
+  // Process in order, since later animations override earlier ones.
+  for (PropertyAnimation& animation : propertyAnimationGroup.mAnimations) {
     MOZ_ASSERT(
-        (!animation.originTime().IsNull() &&
-         animation.startTime().type() == MaybeTimeDuration::TTimeDuration) ||
-            animation.isNotPlaying(),
-        "If we are playing, we should have an origin time and a start"
-        " time");
+        (!animation.mOriginTime.IsNull() &&
+         animation.mStartTime.type() == MaybeTimeDuration::TTimeDuration) ||
+            animation.mIsNotPlaying,
+        "If we are playing, we should have an origin time and a start time");
 
     // Determine if the animation was play-pending and used a ready time later
     // than the previous frame time.
     //
     // To determine this, _all_ of the following consitions need to hold:
     //
     // * There was no previous animation value (i.e. this is the first frame for
     //   the animation since it was sent to the compositor), and
     // * The animation is playing, and
     // * There is a previous frame time, and
     // * The ready time of the animation is ahead of the previous frame time.
     //
     bool hasFutureReadyTime = false;
-    if (!aPreviousValue && !animation.isNotPlaying() &&
+    if (!aPreviousValue && !animation.mIsNotPlaying &&
         !aPreviousFrameTime.IsNull()) {
       // This is the inverse of the calculation performed in
       // AnimationInfo::StartPendingAnimations to calculate the start time of
       // play-pending animations.
       // Note that we have to calculate (TimeStamp + TimeDuration) last to avoid
       // underflow in the middle of the calulation.
       const TimeStamp readyTime =
-          animation.originTime() +
-          (animation.startTime().get_TimeDuration() +
-           animation.holdTime().MultDouble(1.0 / animation.playbackRate()));
+          animation.mOriginTime +
+          (animation.mStartTime.get_TimeDuration() +
+           animation.mHoldTime.MultDouble(1.0 / animation.mPlaybackRate));
       hasFutureReadyTime =
           !readyTime.IsNull() && readyTime > aPreviousFrameTime;
     }
     // Use the previous vsync time to make main thread animations and compositor
     // more closely aligned.
     //
     // On the first frame where we have animations the previous timestamp will
     // not be set so we simply use the current timestamp.  As a result we will
@@ -207,124 +212,122 @@ AnimationHelper::SampleResult AnimationH
     // jumping backwards into the range prior to when the animation starts.
     const TimeStamp& timeStamp =
         aPreviousFrameTime.IsNull() || hasFutureReadyTime ? aCurrentFrameTime
                                                           : aPreviousFrameTime;
 
     // 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.startTime().type() != MaybeTimeDuration::TTimeDuration
-            ? animation.holdTime()
-            : (timeStamp - animation.originTime() -
-               animation.startTime().get_TimeDuration())
-                  .MultDouble(animation.playbackRate());
+        animation.mIsNotPlaying ||
+                animation.mStartTime.type() != MaybeTimeDuration::TTimeDuration
+            ? animation.mHoldTime
+            : (timeStamp - animation.mOriginTime -
+               animation.mStartTime.get_TimeDuration())
+                  .MultDouble(animation.mPlaybackRate);
 
     ComputedTiming computedTiming = dom::AnimationEffect::GetComputedTimingAt(
-        dom::Nullable<TimeDuration>(elapsedDuration), animData.mTiming,
-        animation.playbackRate());
+        dom::Nullable<TimeDuration>(elapsedDuration), animation.mTiming,
+        animation.mPlaybackRate);
 
     if (computedTiming.mProgress.IsNull()) {
       continue;
     }
 
     dom::IterationCompositeOperation iterCompositeOperation =
-        static_cast<dom::IterationCompositeOperation>(
-            animation.iterationComposite());
+        animation.mIterationComposite;
 
     // Skip caluculation if the progress hasn't changed since the last
     // calculation.
     // Note that we don't skip calculate this animation if there is another
     // animation since the other animation might be 'accumulate' or 'add', or
     // might have a missing keyframe (i.e. this animation value will be used in
     // the missing keyframe).
     // FIXME Bug 1455476: We should do this optimizations for the case where
     // the layer has multiple animations.
-    if (iEnd == 1 && !dom::KeyframeEffect::HasComputedTimingChanged(
-                         computedTiming, iterCompositeOperation,
-                         animData.mProgressOnLastCompose,
-                         animData.mCurrentIterationOnLastCompose)) {
+    if (isSingleAnimation && !dom::KeyframeEffect::HasComputedTimingChanged(
+                                 computedTiming, iterCompositeOperation,
+                                 animation.mProgressOnLastCompose,
+                                 animation.mCurrentIterationOnLastCompose)) {
 #ifdef DEBUG
       shouldBeSkipped = true;
 #else
       return SampleResult::Skipped;
 #endif
     }
 
     uint32_t segmentIndex = 0;
-    size_t segmentSize = animation.segments().Length();
-    AnimationSegment* segment = animation.segments().Elements();
-    while (segment->endPortion() < computedTiming.mProgress.Value() &&
+    size_t segmentSize = animation.mSegments.Length();
+    PropertyAnimation::SegmentData* segment = animation.mSegments.Elements();
+    while (segment->mEndPortion < computedTiming.mProgress.Value() &&
            segmentIndex < segmentSize - 1) {
       ++segment;
       ++segmentIndex;
     }
 
     double positionInSegment =
-        (computedTiming.mProgress.Value() - segment->startPortion()) /
-        (segment->endPortion() - segment->startPortion());
+        (computedTiming.mProgress.Value() - segment->mStartPortion) /
+        (segment->mEndPortion - segment->mStartPortion);
 
     double portion = ComputedTimingFunction::GetPortion(
-        animData.mFunctions[segmentIndex], positionInSegment,
-        computedTiming.mBeforeFlag);
+        segment->mFunction, positionInSegment, computedTiming.mBeforeFlag);
 
     // Like above optimization, skip caluculation if the target segment isn't
     // changed and if the portion in the segment isn't changed.
     // This optimization is needed for CSS animations/transitions with step
     // timing functions (e.g. the throbber animation on tab or frame based
     // animations).
     // FIXME Bug 1455476: Like the above optimization, we should apply this
     // optimizations for multiple animation cases as well.
-    if (iEnd == 1 && animData.mSegmentIndexOnLastCompose == segmentIndex &&
-        !animData.mPortionInSegmentOnLastCompose.IsNull() &&
-        animData.mPortionInSegmentOnLastCompose.Value() == portion) {
+    if (isSingleAnimation &&
+        animation.mSegmentIndexOnLastCompose == segmentIndex &&
+        !animation.mPortionInSegmentOnLastCompose.IsNull() &&
+        animation.mPortionInSegmentOnLastCompose.Value() == portion) {
 #ifdef DEBUG
       shouldBeSkipped = true;
 #else
       return SampleResult::Skipped;
 #endif
     }
 
     AnimationPropertySegment animSegment;
     animSegment.mFromKey = 0.0;
     animSegment.mToKey = 1.0;
-    animSegment.mFromValue =
-        AnimationValue(animData.mStartValues[segmentIndex]);
-    animSegment.mToValue = AnimationValue(animData.mEndValues[segmentIndex]);
-    animSegment.mFromComposite =
-        static_cast<dom::CompositeOperation>(segment->startComposite());
-    animSegment.mToComposite =
-        static_cast<dom::CompositeOperation>(segment->endComposite());
+    animSegment.mFromValue = AnimationValue(segment->mStartValue);
+    animSegment.mToValue = AnimationValue(segment->mEndValue);
+    animSegment.mFromComposite = segment->mStartComposite;
+    animSegment.mToComposite = segment->mEndComposite;
 
     // interpolate the property
     aAnimationValue =
         Servo_ComposeAnimationSegment(
-            &animSegment, aAnimationValue, animData.mEndValues.LastElement(),
-            iterCompositeOperation, portion, computedTiming.mCurrentIteration)
+            &animSegment, aAnimationValue,
+            animation.mSegments.LastElement().mEndValue, iterCompositeOperation,
+            portion, computedTiming.mCurrentIteration)
             .Consume();
 
 #ifdef DEBUG
     if (shouldBeSkipped) {
       return SampleResult::Skipped;
     }
 #endif
 
     hasInEffectAnimations = true;
-    animData.mProgressOnLastCompose = computedTiming.mProgress;
-    animData.mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration;
-    animData.mSegmentIndexOnLastCompose = segmentIndex;
-    animData.mPortionInSegmentOnLastCompose.SetValue(portion);
+    animation.mProgressOnLastCompose = computedTiming.mProgress;
+    animation.mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration;
+    animation.mSegmentIndexOnLastCompose = segmentIndex;
+    animation.mPortionInSegmentOnLastCompose.SetValue(portion);
   }
 
 #ifdef DEBUG
   // Sanity check that all of animation data are the same.
-  const AnimationData& lastData = aAnimations.LastElement().data();
-  for (const Animation& animation : aAnimations) {
-    const AnimationData& data = animation.data();
+  const AnimationData& lastData =
+      aPropertyAnimationGroups.LastElement().mAnimationData;
+  for (const PropertyAnimationGroup& group : aPropertyAnimationGroups) {
+    const AnimationData& data = group.mAnimationData;
     MOZ_ASSERT(data.type() == lastData.type(),
                "The type of AnimationData should be the same");
     if (data.type() == AnimationData::Tnull_t) {
       continue;
     }
 
     MOZ_ASSERT(data.type() == AnimationData::TTransformData);
     const TransformData& transformData = data.get_TransformData();
@@ -337,83 +340,105 @@ AnimationHelper::SampleResult AnimationH
                        lastTransformData.appUnitsPerDevPixel(),
                "All of members of TransformData should be the same");
   }
 #endif
 
   return hasInEffectAnimations ? SampleResult::Sampled : SampleResult::None;
 }
 
-void AnimationHelper::SetAnimations(
-    AnimationArray& aAnimations, InfallibleTArray<AnimData>& aAnimData,
-    RefPtr<RawServoAnimationValue>& aBaseAnimationStyle) {
-  for (uint32_t i = 0; i < aAnimations.Length(); i++) {
-    Animation& animation = aAnimations[i];
-    // Adjust fill mode so that if the main thread is delayed in clearing
-    // this animation we don't introduce flicker by jumping back to the old
-    // underlying value.
-    switch (static_cast<dom::FillMode>(animation.fillMode())) {
-      case dom::FillMode::None:
-        if (animation.playbackRate() > 0) {
-          animation.fillMode() = static_cast<uint8_t>(dom::FillMode::Forwards);
-        } else if (animation.playbackRate() < 0) {
-          animation.fillMode() = static_cast<uint8_t>(dom::FillMode::Backwards);
-        }
-        break;
-      case dom::FillMode::Backwards:
-        if (animation.playbackRate() > 0) {
-          animation.fillMode() = static_cast<uint8_t>(dom::FillMode::Both);
-        }
-        break;
-      case dom::FillMode::Forwards:
-        if (animation.playbackRate() < 0) {
-          animation.fillMode() = static_cast<uint8_t>(dom::FillMode::Both);
-        }
-        break;
-      default:
-        break;
-    }
+static dom::FillMode GetAdjustedFillMode(const Animation& aAnimation) {
+  // Adjust fill mode so that if the main thread is delayed in clearing
+  // this animation we don't introduce flicker by jumping back to the old
+  // underlying value.
+  auto fillMode = static_cast<dom::FillMode>(aAnimation.fillMode());
+  float playbackRate = aAnimation.playbackRate();
+  switch (fillMode) {
+    case dom::FillMode::None:
+      if (playbackRate > 0) {
+        fillMode = dom::FillMode::Forwards;
+      } else if (playbackRate < 0) {
+        fillMode = dom::FillMode::Backwards;
+      }
+      break;
+    case dom::FillMode::Backwards:
+      if (playbackRate > 0) {
+        fillMode = dom::FillMode::Both;
+      }
+      break;
+    case dom::FillMode::Forwards:
+      if (playbackRate < 0) {
+        fillMode = dom::FillMode::Both;
+      }
+      break;
+    default:
+      break;
+  }
+  return fillMode;
+}
 
+nsTArray<PropertyAnimationGroup> AnimationHelper::ExtractAnimations(
+    const AnimationArray& aAnimations) {
+  nsTArray<PropertyAnimationGroup> propertyAnimationGroupArray;
+  // FIXME: We only have one entry for now. In the next patch, we will extend
+  // this to multiple properties.
+  auto* propertyAnimationGroup = propertyAnimationGroupArray.AppendElement();
+  if (!aAnimations.IsEmpty()) {
+    propertyAnimationGroup->mProperty = aAnimations.LastElement().property();
+    propertyAnimationGroup->mAnimationData = aAnimations.LastElement().data();
+  }
+
+  for (const Animation& animation : aAnimations) {
     if (animation.baseStyle().type() != Animatable::Tnull_t) {
-      aBaseAnimationStyle = AnimationValue::FromAnimatable(
+      propertyAnimationGroup->mBaseStyle = AnimationValue::FromAnimatable(
           animation.property(), animation.baseStyle());
     }
 
-    AnimData* data = aAnimData.AppendElement();
+    PropertyAnimation* propertyAnimation =
+        propertyAnimationGroup->mAnimations.AppendElement();
 
-    data->mTiming =
+    propertyAnimation->mOriginTime = animation.originTime();
+    propertyAnimation->mStartTime = animation.startTime();
+    propertyAnimation->mHoldTime = animation.holdTime();
+    propertyAnimation->mPlaybackRate = animation.playbackRate();
+    propertyAnimation->mIterationComposite =
+        static_cast<dom::IterationCompositeOperation>(
+            animation.iterationComposite());
+    propertyAnimation->mIsNotPlaying = animation.isNotPlaying();
+    propertyAnimation->mTiming =
         TimingParams{animation.duration(),
                      animation.delay(),
                      animation.endDelay(),
                      animation.iterations(),
                      animation.iterationStart(),
                      static_cast<dom::PlaybackDirection>(animation.direction()),
-                     static_cast<dom::FillMode>(animation.fillMode()),
+                     GetAdjustedFillMode(animation),
                      AnimationUtils::TimingFunctionToComputedTimingFunction(
                          animation.easingFunction())};
-    InfallibleTArray<Maybe<ComputedTimingFunction>>& functions =
-        data->mFunctions;
-    InfallibleTArray<RefPtr<RawServoAnimationValue>>& startValues =
-        data->mStartValues;
-    InfallibleTArray<RefPtr<RawServoAnimationValue>>& endValues =
-        data->mEndValues;
 
-    const InfallibleTArray<AnimationSegment>& segments = animation.segments();
-    for (const AnimationSegment& segment : segments) {
-      startValues.AppendElement(AnimationValue::FromAnimatable(
-          animation.property(), segment.startState()));
-      endValues.AppendElement(AnimationValue::FromAnimatable(
-          animation.property(), segment.endState()));
-
-      TimingFunction tf = segment.sampleFn();
-      Maybe<ComputedTimingFunction> ctf =
-          AnimationUtils::TimingFunctionToComputedTimingFunction(tf);
-      functions.AppendElement(ctf);
+    nsTArray<PropertyAnimation::SegmentData>& segmentData =
+        propertyAnimation->mSegments;
+    for (const AnimationSegment& segment : animation.segments()) {
+      segmentData.AppendElement(PropertyAnimation::SegmentData{
+          AnimationValue::FromAnimatable(animation.property(),
+                                         segment.startState()),
+          AnimationValue::FromAnimatable(animation.property(),
+                                         segment.endState()),
+          AnimationUtils::TimingFunctionToComputedTimingFunction(
+              segment.sampleFn()),
+          segment.startPortion(), segment.endPortion(),
+          static_cast<dom::CompositeOperation>(segment.startComposite()),
+          static_cast<dom::CompositeOperation>(segment.endComposite())});
     }
   }
+
+  if (propertyAnimationGroup->IsEmpty()) {
+    propertyAnimationGroupArray.Clear();
+  }
+  return propertyAnimationGroupArray;
 }
 
 uint64_t AnimationHelper::GetNextCompositorAnimationsId() {
   static uint32_t sNextId = 0;
   ++sNextId;
 
   uint32_t procId = static_cast<uint32_t>(base::GetCurrentProcId());
   uint64_t nextId = procId;
@@ -430,46 +455,46 @@ bool AnimationHelper::SampleAnimations(C
   // Do nothing if there are no compositor animations
   if (!aStorage->AnimationsCount()) {
     return isAnimating;
   }
 
   // Sample the animations in CompositorAnimationStorage
   for (auto iter = aStorage->ConstAnimationsTableIter(); !iter.Done();
        iter.Next()) {
-    AnimationArray* animations = iter.UserData();
-    if (animations->IsEmpty()) {
+    auto& propertyAnimationGroups = *iter.UserData();
+    if (propertyAnimationGroups.IsEmpty()) {
       continue;
     }
 
     isAnimating = true;
     RefPtr<RawServoAnimationValue> animationValue;
-    InfallibleTArray<AnimData> animationData;
-    AnimationHelper::SetAnimations(*animations, animationData, animationValue);
     AnimatedValue* previousValue = aStorage->GetAnimatedValue(iter.Key());
     AnimationHelper::SampleResult sampleResult =
         AnimationHelper::SampleAnimationForEachNode(
-            aPreviousFrameTime, aCurrentFrameTime, *animations, animationData,
+            aPreviousFrameTime, aCurrentFrameTime, propertyAnimationGroups,
             animationValue, previousValue);
 
     if (sampleResult != AnimationHelper::SampleResult::Sampled) {
       continue;
     }
 
+    const PropertyAnimationGroup& lastPropertyAnimationGroup =
+        propertyAnimationGroups.LastElement();
+
     // Store the AnimatedValue
-    Animation& animation = animations->LastElement();
-    switch (animation.property()) {
+    switch (lastPropertyAnimationGroup.mProperty) {
       case eCSSProperty_opacity: {
         aStorage->SetAnimatedValue(
             iter.Key(), Servo_AnimationValue_GetOpacity(animationValue));
         break;
       }
       case eCSSProperty_transform: {
         const TransformData& transformData =
-            animation.data().get_TransformData();
+            lastPropertyAnimationGroup.mAnimationData.get_TransformData();
 
         gfx::Matrix4x4 transform =
             ServoAnimationValueToMatrix4x4(animationValue, transformData);
         gfx::Matrix4x4 frameTransform = transform;
         // If the parent has perspective transform, then the offset into
         // reference frame coordinates is already on this transform. If not,
         // then we need to ask for it to be added here.
         if (!transformData.hasPerspectiveParent()) {
--- a/gfx/layers/AnimationHelper.h
+++ b/gfx/layers/AnimationHelper.h
@@ -11,35 +11,70 @@
 #include "mozilla/ComputedTimingFunction.h"  // for ComputedTimingFunction
 #include "mozilla/layers/LayersMessages.h"   // for TransformData, etc
 #include "mozilla/TimeStamp.h"               // for TimeStamp
 #include "mozilla/TimingParams.h"
 #include "X11UndefineNone.h"
 
 namespace mozilla {
 struct AnimationValue;
+
+namespace dom {
+enum class CompositeOperation : uint8_t;
+enum class IterationCompositeOperation : uint8_t;
+};  // namespace dom
+
 namespace layers {
 class Animation;
 
-typedef InfallibleTArray<layers::Animation> AnimationArray;
+typedef nsTArray<layers::Animation> AnimationArray;
 
-struct AnimData {
-  InfallibleTArray<RefPtr<RawServoAnimationValue>> mStartValues;
-  InfallibleTArray<RefPtr<RawServoAnimationValue>> mEndValues;
-  InfallibleTArray<Maybe<mozilla::ComputedTimingFunction>> mFunctions;
+struct PropertyAnimation {
+  struct SegmentData {
+    RefPtr<RawServoAnimationValue> mStartValue;
+    RefPtr<RawServoAnimationValue> mEndValue;
+    Maybe<mozilla::ComputedTimingFunction> mFunction;
+    float mStartPortion;
+    float mEndPortion;
+    dom::CompositeOperation mStartComposite;
+    dom::CompositeOperation mEndComposite;
+  };
+  nsTArray<SegmentData> mSegments;
   TimingParams mTiming;
+
   // These two variables correspond to the variables of the same name in
   // KeyframeEffectReadOnly and are used for the same purpose: to skip composing
   // animations whose progress has not changed.
   dom::Nullable<double> mProgressOnLastCompose;
   uint64_t mCurrentIterationOnLastCompose = 0;
   // These two variables are used for a similar optimization above but are
   // applied to the timing function in each keyframe.
   uint32_t mSegmentIndexOnLastCompose = 0;
   dom::Nullable<double> mPortionInSegmentOnLastCompose;
+
+  TimeStamp mOriginTime;
+  MaybeTimeDuration mStartTime;
+  TimeDuration mHoldTime;
+  float mPlaybackRate;
+  dom::IterationCompositeOperation mIterationComposite;
+  bool mIsNotPlaying;
+};
+
+struct PropertyAnimationGroup {
+  nsCSSPropertyID mProperty;
+  AnimationData mAnimationData;
+
+  nsTArray<PropertyAnimation> mAnimations;
+  RefPtr<RawServoAnimationValue> mBaseStyle;
+
+  bool IsEmpty() const { return mAnimations.IsEmpty(); }
+  void Clear() {
+    mAnimations.Clear();
+    mBaseStyle = nullptr;
+  }
 };
 
 struct AnimationTransform {
   /*
    * This transform is calculated from sampleanimation in device pixel
    * and used by compositor.
    */
   gfx::Matrix4x4 mTransformInDevSpace;
@@ -91,17 +126,18 @@ struct AnimatedValue {
 //
 // Each layer which has animations gets a CompositorAnimationsId key, and reuses
 // that key during its lifetime. Likewise, in layers-free webrender, a display
 // item that is animated (e.g. nsDisplayTransform) gets a CompositorAnimationsId
 // key and reuses that key (it persists the key via the frame user-data
 // mechanism).
 class CompositorAnimationStorage final {
   typedef nsClassHashtable<nsUint64HashKey, AnimatedValue> AnimatedValueTable;
-  typedef nsClassHashtable<nsUint64HashKey, AnimationArray> AnimationsTable;
+  typedef nsClassHashtable<nsUint64HashKey, nsTArray<PropertyAnimationGroup>>
+      AnimationsTable;
 
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CompositorAnimationStorage)
  public:
   /**
    * Set the animation transform based on the unique id and also
    * set up |aFrameTransform| and |aData| for OMTA testing
    */
   void SetAnimatedValue(uint64_t aId, gfx::Matrix4x4&& aTransformInDevSpace,
@@ -142,17 +178,17 @@ class CompositorAnimationStorage final {
   /**
    * Set the animations based on the unique id
    */
   void SetAnimations(uint64_t aId, const AnimationArray& aAnimations);
 
   /**
    * Return the animations if a given id can map to its animations
    */
-  AnimationArray* GetAnimations(const uint64_t& aId) const;
+  nsTArray<PropertyAnimationGroup>* GetAnimations(const uint64_t& aId) const;
 
   /**
    * Return the iterator of animations table
    */
   AnimationsTable::Iterator ConstAnimationsTableIter() const {
     return mAnimations.ConstIter();
   }
 
@@ -193,26 +229,26 @@ class AnimationHelper {
    * Returns SampleResult::None if none of the animations are producing a result
    * (e.g. they are in the delay phase with no backwards fill),
    * SampleResult::Skipped if the animation output did not change since the last
    * call of this function,
    * SampleResult::Sampled if the animation output was updated.
    */
   static SampleResult SampleAnimationForEachNode(
       TimeStamp aPreviousFrameTime, TimeStamp aCurrentFrameTime,
-      AnimationArray& aAnimations, InfallibleTArray<AnimData>& aAnimationData,
+      nsTArray<PropertyAnimationGroup>& aPropertyAnimationGroups,
       RefPtr<RawServoAnimationValue>& aAnimationValue,
       const AnimatedValue* aPreviousValue);
+
   /**
-   * Populates AnimData stuctures into |aAnimData| and |aBaseAnimationStyle|
-   * based on |aAnimations|.
+   * Extract organized animation data by property into an array of
+   * PropertyAnimationGroup.
    */
-  static void SetAnimations(
-      AnimationArray& aAnimations, InfallibleTArray<AnimData>& aAnimData,
-      RefPtr<RawServoAnimationValue>& aBaseAnimationStyle);
+  static nsTArray<PropertyAnimationGroup> ExtractAnimations(
+      const AnimationArray& aAnimations);
 
   /**
    * Get a unique id to represent the compositor animation between child
    * and parent side. This id will be used as a key to store animation
    * data in the CompositorAnimationStorage per compositor.
    * Each layer on the content side calls this when it gets new animation
    * data.
    */
@@ -222,16 +258,18 @@ class AnimationHelper {
    * Sample animation based a given time stamp |aTime| and the animation
    * data inside CompositorAnimationStorage |aStorage|. The animated values
    * after sampling will be stored in CompositorAnimationStorage as well.
    *
    * Returns true if there is any animation.
    * Note that even if there are only in-delay phase animations (i.e. not
    * visually effective), this function returns true to ensure we composite
    * again on the next tick.
+   *
+   * Note: This is called only by WebRender.
    */
   static bool SampleAnimations(CompositorAnimationStorage* aStorage,
                                TimeStamp aPreviousFrameTime,
                                TimeStamp aCurrentFrameTime);
 
   /**
    * Convert an animation value into a matrix given the corresponding transform
    * parameters. |aValue| must be a transform-like value (e.g. transform,
--- a/gfx/layers/AnimationInfo.cpp
+++ b/gfx/layers/AnimationInfo.cpp
@@ -3,16 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "AnimationInfo.h"
 #include "mozilla/LayerAnimationInfo.h"
 #include "mozilla/layers/WebRenderLayerManager.h"
 #include "mozilla/layers/AnimationHelper.h"
+#include "mozilla/layers/CompositorThread.h"
 #include "mozilla/dom/Animation.h"
 #include "nsIContent.h"
 #include "PuppetWidget.h"
 
 namespace mozilla {
 namespace layers {
 
 AnimationInfo::AnimationInfo() : mCompositorAnimationsId(0), mMutated(false) {}
@@ -21,67 +22,67 @@ AnimationInfo::~AnimationInfo() {}
 
 void AnimationInfo::EnsureAnimationsId() {
   if (!mCompositorAnimationsId) {
     mCompositorAnimationsId = AnimationHelper::GetNextCompositorAnimationsId();
   }
 }
 
 Animation* AnimationInfo::AddAnimation() {
+  MOZ_ASSERT(!CompositorThreadHolder::IsInCompositorThread());
   // Here generates a new id when the first animation is added and
   // this id is used to represent the animations in this layer.
   EnsureAnimationsId();
 
   MOZ_ASSERT(!mPendingAnimations, "should have called ClearAnimations first");
 
   Animation* anim = mAnimations.AppendElement();
 
   mMutated = true;
 
   return anim;
 }
 
 Animation* AnimationInfo::AddAnimationForNextTransaction() {
+  MOZ_ASSERT(!CompositorThreadHolder::IsInCompositorThread());
   MOZ_ASSERT(mPendingAnimations,
              "should have called ClearAnimationsForNextTransaction first");
 
   Animation* anim = mPendingAnimations->AppendElement();
 
   return anim;
 }
 
 void AnimationInfo::ClearAnimations() {
   mPendingAnimations = nullptr;
 
-  if (mAnimations.IsEmpty() && mAnimationData.IsEmpty()) {
+  if (mAnimations.IsEmpty() && mPropertyAnimationGroups.IsEmpty()) {
     return;
   }
 
   mAnimations.Clear();
-  mAnimationData.Clear();
+  mPropertyAnimationGroups.Clear();
 
   mMutated = true;
 }
 
 void AnimationInfo::ClearAnimationsForNextTransaction() {
   // Ensure we have a non-null mPendingAnimations to mark a future clear.
   if (!mPendingAnimations) {
     mPendingAnimations = new AnimationArray;
   }
 
   mPendingAnimations->Clear();
 }
 
 void AnimationInfo::SetCompositorAnimations(
     const CompositorAnimations& aCompositorAnimations) {
-  mAnimations = aCompositorAnimations.animations();
   mCompositorAnimationsId = aCompositorAnimations.id();
-  mAnimationData.Clear();
-  AnimationHelper::SetAnimations(mAnimations, mAnimationData,
-                                 mBaseAnimationStyle);
+  mPropertyAnimationGroups =
+      AnimationHelper::ExtractAnimations(aCompositorAnimations.animations());
 }
 
 bool AnimationInfo::StartPendingAnimations(const TimeStamp& aReadyTime) {
   bool updated = false;
   for (size_t animIdx = 0, animEnd = mAnimations.Length(); animIdx < animEnd;
        animIdx++) {
     Animation& anim = mAnimations[animIdx];
 
--- a/gfx/layers/AnimationInfo.h
+++ b/gfx/layers/AnimationInfo.h
@@ -18,20 +18,20 @@ class nsIFrame;
 
 namespace mozilla {
 namespace layers {
 
 class Animation;
 class CompositorAnimations;
 class Layer;
 class LayerManager;
-struct AnimData;
+struct PropertyAnimationGroup;
 
 class AnimationInfo final {
-  typedef InfallibleTArray<Animation> AnimationArray;
+  typedef nsTArray<Animation> AnimationArray;
 
  public:
   AnimationInfo();
   ~AnimationInfo();
 
   // Ensure that this AnimationInfo has a valid (non-zero) animations id. This
   // value is unique across layers.
   void EnsureAnimationsId();
@@ -58,21 +58,22 @@ class AnimationInfo final {
   void ClearAnimations();
   void ClearAnimationsForNextTransaction();
   void SetCompositorAnimations(
       const CompositorAnimations& aCompositorAnimations);
   bool StartPendingAnimations(const TimeStamp& aReadyTime);
   void TransferMutatedFlagToLayer(Layer* aLayer);
 
   uint64_t GetCompositorAnimationsId() { return mCompositorAnimationsId; }
-  RawServoAnimationValue* GetBaseAnimationStyle() const {
-    return mBaseAnimationStyle;
+  // Note: We don't set mAnimations on the compositor thread, so this will
+  // always return an empty array on the compositor thread.
+  AnimationArray& GetAnimations() { return mAnimations; }
+  nsTArray<PropertyAnimationGroup>& GetPropertyAnimationGroups() {
+    return mPropertyAnimationGroups;
   }
-  InfallibleTArray<AnimData>& GetAnimationData() { return mAnimationData; }
-  AnimationArray& GetAnimations() { return mAnimations; }
   bool ApplyPendingUpdatesForThisTransaction();
   bool HasTransformAnimation() const;
 
   // In case of continuation, |aFrame| must be the first or the last
   // continuation frame, otherwise this function might return Nothing().
   static Maybe<uint64_t> GetGenerationFromFrame(
       nsIFrame* aFrame, DisplayItemType aDisplayItemKey);
 
@@ -85,23 +86,35 @@ class AnimationInfo final {
   //
   // The enumeration stops if |aCallback| returns false.
   static void EnumerateGenerationOnFrame(
       const nsIFrame* aFrame, const nsIContent* aContent,
       const CompositorAnimatableDisplayItemTypes& aDisplayItemTypes,
       const AnimationGenerationCallback& aCallback);
 
  protected:
+  // mAnimations (and mPendingAnimations) are only set on the main thread.
+  //
+  // Once the animations are received on the compositor thread/process we
+  // use AnimationHelper::ExtractAnimations to transform the rather compact
+  // representation of animation data we transfer into something we can more
+  // readily use for sampling and then store it in mPropertyAnimationGroups
+  // (below) or CompositorAnimationStorage.mAnimations for WebRender.
   AnimationArray mAnimations;
+  nsAutoPtr<AnimationArray> mPendingAnimations;
+
   uint64_t mCompositorAnimationsId;
-  nsAutoPtr<AnimationArray> mPendingAnimations;
-  InfallibleTArray<AnimData> mAnimationData;
+  // The extracted data produced by AnimationHelper::ExtractAnimations().
+  //
+  // Each entry in the array represents an animation list for one property.  For
+  // transform-like properties (e.g. transform, rotate etc.), there may be
+  // multiple entries depending on how many transform-like properties we have.
+  nsTArray<PropertyAnimationGroup> mPropertyAnimationGroups;
   // If this layer is used for OMTA, then this counter is used to ensure we
   // stay in sync with the animation manager
   Maybe<uint64_t> mAnimationGeneration;
-  RefPtr<RawServoAnimationValue> mBaseAnimationStyle;
   bool mMutated;
 };
 
 }  // namespace layers
 }  // namespace mozilla
 
 #endif  // GFX_ANIMATIONINFO_H
--- a/gfx/layers/Layers.cpp
+++ b/gfx/layers/Layers.cpp
@@ -758,20 +758,16 @@ bool Layer::GetVisibleRegionRelativeToRo
     // positioning code.
     offset += currentLayerOffset;
   }
 
   *aLayerOffset = IntPoint(offset.x, offset.y);
   return true;
 }
 
-InfallibleTArray<AnimData>& Layer::GetAnimationData() {
-  return mAnimationInfo.GetAnimationData();
-}
-
 Maybe<ParentLayerIntRect> Layer::GetCombinedClipRect() const {
   Maybe<ParentLayerIntRect> clip = GetClipRect();
 
   clip = IntersectMaybeRects(clip, GetScrolledClipRect());
 
   for (size_t i = 0; i < mScrollMetadata.Length(); i++) {
     clip = IntersectMaybeRects(clip, mScrollMetadata[i].GetClipRect());
   }
@@ -1768,16 +1764,22 @@ void Layer::PrintInfo(std::stringstream&
     aStream << nsPrintfCString(" [mMaskLayer=%p]", mMaskLayer.get()).get();
   }
   for (uint32_t i = 0; i < mScrollMetadata.Length(); i++) {
     if (!mScrollMetadata[i].IsDefault()) {
       aStream << nsPrintfCString(" [metrics%d=", i).get();
       AppendToString(aStream, mScrollMetadata[i], "", "]");
     }
   }
+  // FIXME: On the compositor thread, we don't set mAnimationInfo::mAnimations,
+  // All animations are transformed by AnimationHelper::ExtractAnimations() into
+  // mAnimationInfo.mPropertyAnimationGroups, instead. So if we want to check
+  // if layer trees are properly synced up across processes, we should dump
+  // mAnimationInfo.mPropertyAnimationGroups for the compositor thread.
+  // (See AnimationInfo.h for more details.)
   if (!mAnimationInfo.GetAnimations().IsEmpty()) {
     aStream << nsPrintfCString(" [%d animations with id=%" PRIu64 " ]",
                                (int)mAnimationInfo.GetAnimations().Length(),
                                mAnimationInfo.GetCompositorAnimationsId())
                    .get();
   }
 }
 
--- a/gfx/layers/Layers.h
+++ b/gfx/layers/Layers.h
@@ -802,17 +802,17 @@ class LayerManager : public FrameRecorde
 
 /**
  * A Layer represents anything that can be rendered onto a destination
  * surface.
  */
 class Layer {
   NS_INLINE_DECL_REFCOUNTING(Layer)
 
-  typedef InfallibleTArray<Animation> AnimationArray;
+  typedef nsTArray<Animation> AnimationArray;
 
  public:
   // Keep these in alphabetical order
   enum LayerType {
     TYPE_CANVAS,
     TYPE_COLOR,
     TYPE_CONTAINER,
     TYPE_DISPLAYITEM,
@@ -1439,32 +1439,31 @@ class Layer {
    *  in this case, results will not be valid. Returns true on successful
    *  traversal.
    */
   bool GetVisibleRegionRelativeToRootLayer(nsIntRegion& aResult,
                                            nsIntPoint* aLayerOffset);
 
   // Note that all lengths in animation data are either in CSS pixels or app
   // units and must be converted to device pixels by the compositor.
+  // Besides, this should only be called on the compositor thread.
   AnimationArray& GetAnimations() { return mAnimationInfo.GetAnimations(); }
   uint64_t GetCompositorAnimationsId() {
     return mAnimationInfo.GetCompositorAnimationsId();
   }
-  InfallibleTArray<AnimData>& GetAnimationData();
+  nsTArray<PropertyAnimationGroup>& GetPropertyAnimationGroups() {
+    return mAnimationInfo.GetPropertyAnimationGroups();
+  }
 
   Maybe<uint64_t> GetAnimationGeneration() const {
     return mAnimationInfo.GetAnimationGeneration();
   }
 
   bool HasTransformAnimation() const;
 
-  RawServoAnimationValue* GetBaseAnimationStyle() const {
-    return mAnimationInfo.GetBaseAnimationStyle();
-  }
-
   /**
    * 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();
 
   /**
--- a/gfx/layers/composite/AsyncCompositionManager.cpp
+++ b/gfx/layers/composite/AsyncCompositionManager.cpp
@@ -681,41 +681,46 @@ static void ApplyAnimatedValue(Layer* aL
 
 static bool SampleAnimations(Layer* aLayer,
                              CompositorAnimationStorage* aStorage,
                              TimeStamp aPreviousFrameTime,
                              TimeStamp aCurrentFrameTime) {
   bool isAnimating = false;
 
   ForEachNode<ForwardIterator>(aLayer, [&](Layer* layer) {
-    AnimationArray& animations = layer->GetAnimations();
-    if (animations.IsEmpty()) {
+    auto& propertyAnimationGroups = layer->GetPropertyAnimationGroups();
+    if (propertyAnimationGroups.IsEmpty()) {
       return;
     }
+
     isAnimating = true;
     AnimatedValue* previousValue =
         aStorage->GetAnimatedValue(layer->GetCompositorAnimationsId());
-    RefPtr<RawServoAnimationValue> animationValue =
-        layer->GetBaseAnimationStyle();
+
+    RefPtr<RawServoAnimationValue> animationValue;
     AnimationHelper::SampleResult sampleResult =
         AnimationHelper::SampleAnimationForEachNode(
-            aPreviousFrameTime, aCurrentFrameTime, animations,
-            layer->GetAnimationData(), animationValue, previousValue);
+            aPreviousFrameTime, aCurrentFrameTime, propertyAnimationGroups,
+            animationValue, previousValue);
+
+    const PropertyAnimationGroup& lastPropertyAnimationGroup =
+        propertyAnimationGroups.LastElement();
+
     switch (sampleResult) {
       case AnimationHelper::SampleResult::Sampled: {
-        Animation& animation = animations.LastElement();
-        ApplyAnimatedValue(layer, aStorage, animation.property(),
-                           animation.data(), animationValue);
+        ApplyAnimatedValue(
+            layer, aStorage, lastPropertyAnimationGroup.mProperty,
+            lastPropertyAnimationGroup.mAnimationData, animationValue);
         break;
       }
       case AnimationHelper::SampleResult::Skipped:
-        switch (animations[0].property()) {
+        switch (lastPropertyAnimationGroup.mProperty) {
           case eCSSProperty_background_color:
           case eCSSProperty_opacity: {
-            if (animations[0].property() == eCSSProperty_opacity) {
+            if (lastPropertyAnimationGroup.mProperty == eCSSProperty_opacity) {
               MOZ_ASSERT(
                   layer->AsHostLayer()->GetShadowOpacitySetByAnimation());
 #ifdef DEBUG
               // Disable this assertion until the root cause is fixed in bug
               // 1459775.
               // MOZ_ASSERT(FuzzyEqualsMultiplicative(
               //   Servo_AnimationValue_GetOpacity(animationValue),
               //   *(aStorage->GetAnimationOpacity(layer->GetCompositorAnimationsId()))));
@@ -730,17 +735,17 @@ static bool SampleAnimations(Layer* aLay
             break;
           }
           case eCSSProperty_transform: {
             MOZ_ASSERT(
                 layer->AsHostLayer()->GetShadowTransformSetByAnimation());
             MOZ_ASSERT(previousValue);
 #ifdef DEBUG
             const TransformData& transformData =
-                animations[0].data().get_TransformData();
+                lastPropertyAnimationGroup.mAnimationData.get_TransformData();
             Matrix4x4 frameTransform =
                 AnimationHelper::ServoAnimationValueToMatrix4x4(animationValue,
                                                                 transformData);
             Matrix4x4 transformInDevice = FrameTransformToTransformInDevice(
                 frameTransform, layer, transformData);
             MOZ_ASSERT(previousValue->mTransform.mTransformInDevSpace
                            .FuzzyEqualsMultiplicative(transformInDevice));
 #endif
--- a/gfx/layers/ipc/CompositorBridgeParent.cpp
+++ b/gfx/layers/ipc/CompositorBridgeParent.cpp
@@ -883,17 +883,17 @@ void CompositorBridgeParent::SetShadowPr
     for (size_t i = 0; i < layer->GetAncestorMaskLayerCount(); i++) {
       SetShadowProperties(layer->GetAncestorMaskLayerAt(i));
     }
 
     // FIXME: Bug 717688 -- Do these updates in
     // LayerTransactionParent::RecvUpdate.
     HostLayer* layerCompositor = layer->AsHostLayer();
     // Set the layerComposite's base transform to the layer's base transform.
-    AnimationArray& animations = layer->GetAnimations();
+    const auto& animations = layer->GetPropertyAnimationGroups();
     // If there is any animation, the animation value will override
     // non-animated value later, so we don't need to set the non-animated
     // value here.
     if (animations.IsEmpty()) {
       layerCompositor->SetShadowBaseTransform(layer->GetBaseTransform());
       layerCompositor->SetShadowTransformSetByAnimation(false);
       layerCompositor->SetShadowOpacity(layer->GetOpacity());
       layerCompositor->SetShadowOpacitySetByAnimation(false);
--- a/gfx/layers/ipc/LayerTransactionParent.cpp
+++ b/gfx/layers/ipc/LayerTransactionParent.cpp
@@ -532,17 +532,17 @@ bool LayerTransactionParent::SetLayerAtt
     layer->SetMaskLayer(AsLayer(maskLayer));
   } else {
     layer->SetMaskLayer(nullptr);
   }
   layer->SetCompositorAnimations(common.compositorAnimations());
   // Clean up the Animations by id in the CompositorAnimationStorage
   // if there are no active animations on the layer
   if (mAnimStorage && layer->GetCompositorAnimationsId() &&
-      layer->GetAnimations().IsEmpty()) {
+      layer->GetPropertyAnimationGroups().IsEmpty()) {
     mAnimStorage->ClearById(layer->GetCompositorAnimationsId());
   }
   if (common.scrollMetadata() != layer->GetAllScrollMetadata()) {
     UpdateHitTestingTree(layer, "scroll metadata changed");
     layer->SetScrollMetadata(common.scrollMetadata());
   }
   layer->SetDisplayListLog(common.displayListLog().get());
 
@@ -717,28 +717,28 @@ mozilla::ipc::IPCResult LayerTransaction
   // AsyncCompositionManager.cpp.
   if (ContainerLayer* c = layer->AsContainerLayer()) {
     transform.PostScale(1.0f / c->GetInheritedXScale(),
                         1.0f / c->GetInheritedYScale(), 1.0f);
   }
   float scale = 1;
   Point3D scaledOrigin;
   Point3D transformOrigin;
-  for (uint32_t i = 0; i < layer->GetAnimations().Length(); i++) {
-    if (layer->GetAnimations()[i].data().type() ==
-        AnimationData::TTransformData) {
-      const TransformData& data =
-          layer->GetAnimations()[i].data().get_TransformData();
-      scale = data.appUnitsPerDevPixel();
-      scaledOrigin = Point3D(
-          NS_round(NSAppUnitsToFloatPixels(data.origin().x, scale)),
-          NS_round(NSAppUnitsToFloatPixels(data.origin().y, scale)), 0.0f);
-      transformOrigin = data.transformOrigin();
-      break;
+  for (const PropertyAnimationGroup& group :
+       layer->GetPropertyAnimationGroups()) {
+    if (group.mAnimationData.type() != AnimationData::TTransformData) {
+      continue;
     }
+    const TransformData& data = group.mAnimationData.get_TransformData();
+    scale = data.appUnitsPerDevPixel();
+    scaledOrigin = Point3D(
+        NS_round(NSAppUnitsToFloatPixels(data.origin().x, scale)),
+        NS_round(NSAppUnitsToFloatPixels(data.origin().y, scale)), 0.0f);
+    transformOrigin = data.transformOrigin();
+    break;
   }
 
   // If our parent isn't a perspective layer, then the offset into reference
   // frame coordinates will have been applied to us. Add an inverse translation
   // to cancel it out.
   if (!layer->GetParent() || !layer->GetParent()->GetTransformIsPerspective()) {
     transform.PostTranslate(-scaledOrigin.x, -scaledOrigin.y, -scaledOrigin.z);
   }