[Web Animations] Don't schedule animation frames or update style while an accelerated...
authorgraouts@webkit.org <graouts@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 13 Nov 2018 10:22:56 +0000 (10:22 +0000)
committergraouts@webkit.org <graouts@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 13 Nov 2018 10:22:56 +0000 (10:22 +0000)
https://bugs.webkit.org/show_bug.cgi?id=191542
<rdar://problem/45356027>

Reviewed by Simon Fraser.

Source/WebCore:

Test: animations/no-style-recalc-during-accelerated-animation.html

In order to be more power-efficient, we stop scheduling calls to updateAnimationsAndSendEvents() when running only accelerated
animations. To do that, we prevent scheduling further animation resolution if we're in the process of updating animations, and
when we are done, call the new DocumentTimeline::scheduleNextTick() method that will check whether we have only accelerated
animations running, and in that case check which of those animations needs an update the soonest and starts a timer scheduled
for that time when we'll schedule animation resolution.

By default, animations compute the time until their natural completion but in the case of CSS Animations, we want to make sure
we also update animations in-flight to dispatch "animationiteration" events.

* animation/AnimationEffect.h: Make the simpleIterationProgress() public so it can be called by WebAnimation::timeToNextTick().
* animation/DocumentTimeline.cpp:
(WebCore::DocumentTimeline::DocumentTimeline): Create the m_tickScheduleTimer and set it up to call scheduleAnimationResolutionIfNeeded().
(WebCore::DocumentTimeline::suspendAnimations): If we don't already have a cached current time, cache the current time.
(WebCore::DocumentTimeline::resumeAnimations): Reset the cached current time to ensure we'll get a fresh one when updating animations next.
(WebCore::DocumentTimeline::liveCurrentTime const): Factor the code to compute the current time out of currentTime() so that we can
cache the current time in suspendAnimations() without also automatically clearing the current time.
(WebCore::DocumentTimeline::currentTime): Use liveCurrentTime() and cacheCurrentTime() since much of the code from this function has been
factored out into those. Additionally, we were failing to clear the current time if called inside an animation frame, which we now do correctly
by virtue of using cacheCurrentTime(). This fixes some flakiness.
(WebCore::DocumentTimeline::cacheCurrentTime): Factor the code to cache the current time out of currentTime().
(WebCore::DocumentTimeline::maybeClearCachedCurrentTime): No need to clear the current time if we get suspended.
(WebCore::DocumentTimeline::scheduleAnimationResolutionIfNeeded): Prevent scheduling an animation update if we're in the middle of one already,
scheduleNextTick() will be called after animations are updated to see if we should schedule an animation update instead.
(WebCore::DocumentTimeline::unscheduleAnimationResolution): Cancel the m_tickScheduleTimer if we need to unschedule animation resolution.
(WebCore::DocumentTimeline::animationResolutionTimerFired): Factor the call to applyPendingAcceleratedAnimations() out of updateAnimationsAndSendEvents()
and call scheduleNextTick().
(WebCore::DocumentTimeline::updateAnimationsAndSendEvents): Set the new m_isUpdatingAnimations member variable to true while this function is running.
(WebCore::DocumentTimeline::scheduleNextTick): Schedule an animation update immediately if we have any relevant animation that is not accelerated.
Otherwise, iterate through all animations to figure out the earliest moment at which we need to update animations.
(WebCore::DocumentTimeline::updateListOfElementsWithRunningAcceleratedAnimationsForElement): Use the new WebAnimation::isRunningAccelerated() function.
* animation/DocumentTimeline.h:
* animation/WebAnimation.cpp:
(WebCore::WebAnimation::isRunningAccelerated const): Since we end up checking if an animation is running with an accelerated effect, we introduce a new
function to get that information directly through the WebAnimation object without bothering about its effect.
(WebCore::WebAnimation::resolve): We should only call updateFinishedState() here since timingDidChange() would also notify the timeline about a potential
change in relevance, which is not necessary and which would schedule an animation frame even for animations that are accelerated.
(WebCore::WebAnimation::timeToNextTick const): Compute the time until our animation completion or, in the case of CSS animations, the next iteration.
* animation/WebAnimation.h:

LayoutTests:

Add a test that checks that we make only minimal style updates and still dispatch events while an accelerated animation is running.

* animations/no-style-recalc-during-accelerated-animation-expected.txt: Added.
* animations/no-style-recalc-during-accelerated-animation.html: Added.
* fast/layers/no-clipping-overflow-hidden-added-after-transform-expected.html:
* fast/layers/no-clipping-overflow-hidden-added-after-transform.html: Change the colors to avoid a tiny ImageOnlyFailure.
* platform/win/TestExpectations: Mark some regressions tracked by webkit.org/b/191584.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@238128 268f45cc-cd09-0410-ab3c-d52691b4dbfc

12 files changed:
LayoutTests/ChangeLog
LayoutTests/animations/no-style-recalc-during-accelerated-animation-expected.txt [new file with mode: 0644]
LayoutTests/animations/no-style-recalc-during-accelerated-animation.html [new file with mode: 0644]
LayoutTests/fast/layers/no-clipping-overflow-hidden-added-after-transform-expected.html
LayoutTests/fast/layers/no-clipping-overflow-hidden-added-after-transform.html
LayoutTests/platform/win/TestExpectations
Source/WebCore/ChangeLog
Source/WebCore/animation/AnimationEffect.h
Source/WebCore/animation/DocumentTimeline.cpp
Source/WebCore/animation/DocumentTimeline.h
Source/WebCore/animation/WebAnimation.cpp
Source/WebCore/animation/WebAnimation.h

index 36136c4..a16f0b0 100644 (file)
@@ -1,3 +1,19 @@
+2018-11-12  Antoine Quint  <graouts@apple.com>
+
+        [Web Animations] Don't schedule animation frames or update style while an accelerated animation is running
+        https://bugs.webkit.org/show_bug.cgi?id=191542
+        <rdar://problem/45356027>
+
+        Reviewed by Simon Fraser.
+
+        Add a test that checks that we make only minimal style updates and still dispatch events while an accelerated animation is running.
+
+        * animations/no-style-recalc-during-accelerated-animation-expected.txt: Added.
+        * animations/no-style-recalc-during-accelerated-animation.html: Added.
+        * fast/layers/no-clipping-overflow-hidden-added-after-transform-expected.html:
+        * fast/layers/no-clipping-overflow-hidden-added-after-transform.html: Change the colors to avoid a tiny ImageOnlyFailure.
+        * platform/win/TestExpectations: Mark some regressions tracked by webkit.org/b/191584.
+
 2018-11-12  Darshan Kadu  <darsh7807@gmail.com>
 
         Implement Cache API support for WPE/GTK
diff --git a/LayoutTests/animations/no-style-recalc-during-accelerated-animation-expected.txt b/LayoutTests/animations/no-style-recalc-during-accelerated-animation-expected.txt
new file mode 100644 (file)
index 0000000..36537bb
--- /dev/null
@@ -0,0 +1,2 @@
+Got iteration event.
+PASS: saw two or fewer style recalcs during the animation.
diff --git a/LayoutTests/animations/no-style-recalc-during-accelerated-animation.html b/LayoutTests/animations/no-style-recalc-during-accelerated-animation.html
new file mode 100644 (file)
index 0000000..cfa01f7
--- /dev/null
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <style>
+        #box {
+            width: 100px;
+            height: 100px;
+            background-color: blue;
+        }
+
+        .animating {
+            animation: move 150ms linear 2;
+        }
+
+        @keyframes move {
+            from { transform: translateX(0); }
+            to   { transform: translateX(500px); }
+        }
+    </style>
+    <script>
+
+        if (window.testRunner) {
+            testRunner.dumpAsText();
+            testRunner.waitUntilDone();
+        }
+
+        window.addEventListener("load", () => {
+            if (!window.internals || !window.testRunner)
+                return;
+
+            const box = document.getElementById("box");
+            const result = document.getElementById("result");
+
+            box.addEventListener("animationstart", () => internals.startTrackingStyleRecalcs());
+            box.addEventListener("animationiteration", () => result.innerText = "Got iteration event.\n");
+            box.addEventListener("animationend", () => {
+                const numRecalcs = internals.styleRecalcCount();
+                if (numRecalcs > 2)
+                    result.innerText += "FAIL: saw " + numRecalcs + " style recalcs during the animation, should only see two."
+                else 
+                    result.innerText += "PASS: saw two or fewer style recalcs during the animation."
+
+                if (window.testRunner)
+                    testRunner.notifyDone();
+            });
+            box.classList.add("animating");
+        });
+    </script>
+</head>
+<body>
+<div id="box"></div>
+<p id="result"></p>
+</body>
+</html>
index 033e02a..96c17f2 100644 (file)
@@ -9,12 +9,12 @@ div {
 
 #overflowHidden {
     overflow: hidden;
-    background: purple;
+    background: white;
 }
 
 #transformed {
     -webkit-transform: rotate(45deg) translate3d(0, 0, 0);
-    background: green;
+    background: black;
 }
 </style>
 </head>
index 8be0219..7d83347 100644 (file)
@@ -9,13 +9,13 @@ div {
 
 #overflowHidden {
     overflow: hidden;
-    background: purple;
+    background: white;
 }
 
 #transformed {
     -webkit-transform: rotate(0deg) translate3d(0, 0, 0);
     -webkit-transition: -webkit-transform linear 1ms;
-    background: green;
+    background: black;
 }
 
 #transformed:hover {
@@ -32,7 +32,7 @@ div {
 <script>
     function transitionFinished() {
         if (window.testRunner)
-            requestAnimationFrame(() => window.testRunner.notifyDone());
+            window.testRunner.notifyDone();
     }
 
     if (!window.eventSender)
index 91288a5..1ce14e0 100644 (file)
@@ -4251,3 +4251,14 @@ webkit.org/b/191366 fast/block/basic/quirk-mode-percent-height.html [ Failure ]
 webkit.org/b/191368 fast/text/stroking-decorations.html [ Crash ]
 webkit.org/b/191368 imported/blink/fast/text/international/complex-text-trailing-space.html [ Crash ]
 webkit.org/b/191368 imported/blink/fast/text/sub-pixel/complex-text-preferred-width.html [ Crash ]
+
+webkit.org/b/191584 animations/animation-direction-normal.html [ Failure ]
+webkit.org/b/191584 animations/animation-direction-reverse.html [ Failure ]
+webkit.org/b/191584 animations/dynamic-stylesheet-loading.html [ Failure ]
+webkit.org/b/191584 animations/play-state-paused.html [ Failure ]
+webkit.org/b/191584 animations/transform-non-accelerated.html [ Failure ]
+webkit.org/b/191584 transitions/start-transform-transition.html [ Failure ]
+webkit.org/b/191584 http/wpt/css/css-animations/set-animation-play-state-to-paused-001.html [ ImageOnlyFailure ]
+webkit.org/b/191584 webanimations/accelerated-animation-with-delay.html [ ImageOnlyFailure ]
+webkit.org/b/191584 webanimations/accelerated-transition-by-removing-property.html [ ImageOnlyFailure ]
+webkit.org/b/191584 fast/animation/css-animation-resuming-when-visible-with-style-change.html [ Timeout ]
index 48d3f0c..76f9917 100644 (file)
@@ -1,3 +1,52 @@
+2018-11-12  Antoine Quint  <graouts@apple.com>
+
+        [Web Animations] Don't schedule animation frames or update style while an accelerated animation is running
+        https://bugs.webkit.org/show_bug.cgi?id=191542
+        <rdar://problem/45356027>
+
+        Reviewed by Simon Fraser.
+
+        Test: animations/no-style-recalc-during-accelerated-animation.html
+
+        In order to be more power-efficient, we stop scheduling calls to updateAnimationsAndSendEvents() when running only accelerated
+        animations. To do that, we prevent scheduling further animation resolution if we're in the process of updating animations, and
+        when we are done, call the new DocumentTimeline::scheduleNextTick() method that will check whether we have only accelerated
+        animations running, and in that case check which of those animations needs an update the soonest and starts a timer scheduled
+        for that time when we'll schedule animation resolution.
+
+        By default, animations compute the time until their natural completion but in the case of CSS Animations, we want to make sure
+        we also update animations in-flight to dispatch "animationiteration" events.
+
+        * animation/AnimationEffect.h: Make the simpleIterationProgress() public so it can be called by WebAnimation::timeToNextTick().
+        * animation/DocumentTimeline.cpp:
+        (WebCore::DocumentTimeline::DocumentTimeline): Create the m_tickScheduleTimer and set it up to call scheduleAnimationResolutionIfNeeded().
+        (WebCore::DocumentTimeline::suspendAnimations): If we don't already have a cached current time, cache the current time.
+        (WebCore::DocumentTimeline::resumeAnimations): Reset the cached current time to ensure we'll get a fresh one when updating animations next.
+        (WebCore::DocumentTimeline::liveCurrentTime const): Factor the code to compute the current time out of currentTime() so that we can
+        cache the current time in suspendAnimations() without also automatically clearing the current time.
+        (WebCore::DocumentTimeline::currentTime): Use liveCurrentTime() and cacheCurrentTime() since much of the code from this function has been
+        factored out into those. Additionally, we were failing to clear the current time if called inside an animation frame, which we now do correctly
+        by virtue of using cacheCurrentTime(). This fixes some flakiness.
+        (WebCore::DocumentTimeline::cacheCurrentTime): Factor the code to cache the current time out of currentTime(). 
+        (WebCore::DocumentTimeline::maybeClearCachedCurrentTime): No need to clear the current time if we get suspended.
+        (WebCore::DocumentTimeline::scheduleAnimationResolutionIfNeeded): Prevent scheduling an animation update if we're in the middle of one already,
+        scheduleNextTick() will be called after animations are updated to see if we should schedule an animation update instead.
+        (WebCore::DocumentTimeline::unscheduleAnimationResolution): Cancel the m_tickScheduleTimer if we need to unschedule animation resolution.
+        (WebCore::DocumentTimeline::animationResolutionTimerFired): Factor the call to applyPendingAcceleratedAnimations() out of updateAnimationsAndSendEvents()
+        and call scheduleNextTick().
+        (WebCore::DocumentTimeline::updateAnimationsAndSendEvents): Set the new m_isUpdatingAnimations member variable to true while this function is running.
+        (WebCore::DocumentTimeline::scheduleNextTick): Schedule an animation update immediately if we have any relevant animation that is not accelerated.
+        Otherwise, iterate through all animations to figure out the earliest moment at which we need to update animations.
+        (WebCore::DocumentTimeline::updateListOfElementsWithRunningAcceleratedAnimationsForElement): Use the new WebAnimation::isRunningAccelerated() function.
+        * animation/DocumentTimeline.h:
+        * animation/WebAnimation.cpp:
+        (WebCore::WebAnimation::isRunningAccelerated const): Since we end up checking if an animation is running with an accelerated effect, we introduce a new
+        function to get that information directly through the WebAnimation object without bothering about its effect.
+        (WebCore::WebAnimation::resolve): We should only call updateFinishedState() here since timingDidChange() would also notify the timeline about a potential
+        change in relevance, which is not necessary and which would schedule an animation frame even for animations that are accelerated.
+        (WebCore::WebAnimation::timeToNextTick const): Compute the time until our animation completion or, in the case of CSS animations, the next iteration.
+        * animation/WebAnimation.h:
+
 2018-11-13  Miguel Gomez  <magomez@igalia.com>
 
         [GTK][WPE] Incorrect tile coverage when resizing a layer out of the visible area
index e621693..8584db2 100644 (file)
@@ -90,6 +90,7 @@ public:
     std::optional<Seconds> localTime() const;
     std::optional<Seconds> activeTime() const;
     Seconds endTime() const;
+    std::optional<double> simpleIterationProgress() const;
     std::optional<double> iterationProgress() const;
     std::optional<double> currentIteration() const;
     Seconds activeDuration() const;
@@ -104,7 +105,6 @@ private:
     enum class ComputedDirection { Forwards, Reverse };
 
     std::optional<double> overallProgress() const;
-    std::optional<double> simpleIterationProgress() const;
     AnimationEffect::ComputedDirection currentDirection() const;
     std::optional<double> directedProgress() const;
     std::optional<double> transformedProgress() const;
index bab5b83..5381424 100644 (file)
@@ -63,6 +63,7 @@ DocumentTimeline::DocumentTimeline(Document& document, Seconds originTime)
     : AnimationTimeline(DocumentTimelineClass)
     , m_document(&document)
     , m_originTime(originTime)
+    , m_tickScheduleTimer(*this, &DocumentTimeline::scheduleAnimationResolutionIfNeeded)
 #if !USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
     , m_animationResolutionTimer(*this, &DocumentTimeline::animationResolutionTimerFired)
 #endif
@@ -208,6 +209,9 @@ void DocumentTimeline::suspendAnimations()
     if (animationsAreSuspended())
         return;
 
+    if (!m_cachedCurrentTime)
+        m_cachedCurrentTime = liveCurrentTime();
+
     for (const auto& animation : m_animations)
         animation->setSuspended(true);
 
@@ -223,6 +227,8 @@ void DocumentTimeline::resumeAnimations()
     if (!animationsAreSuspended())
         return;
 
+    m_cachedCurrentTime = std::nullopt;
+
     m_isSuspended = false;
 
     for (const auto& animation : m_animations)
@@ -246,6 +252,15 @@ unsigned DocumentTimeline::numberOfActiveAnimationsForTesting() const
     return count;
 }
 
+Seconds DocumentTimeline::liveCurrentTime() const
+{
+#if USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
+    return m_document->animationScheduler().lastTimestamp();
+#else
+    return Seconds(m_document->domWindow()->nowTimestamp());
+#endif
+}
+
 std::optional<Seconds> DocumentTimeline::currentTime()
 {
     if (!m_document || !m_document->domWindow())
@@ -259,12 +274,14 @@ std::optional<Seconds> DocumentTimeline::currentTime()
         }
     }
 
+    auto currentTime = liveCurrentTime();
+
 #if USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
     // If we're in the middle of firing a frame, either due to a requestAnimationFrame callback
     // or scheduling an animation update, we want to ensure we use the same time we're using as
     // the timestamp for requestAnimationFrame() callbacks.
     if (m_document->animationScheduler().isFiring())
-        m_cachedCurrentTime = m_document->animationScheduler().lastTimestamp();
+        cacheCurrentTime(currentTime);
 #endif
 
     if (!m_cachedCurrentTime) {
@@ -273,40 +290,45 @@ std::optional<Seconds> DocumentTimeline::currentTime()
         // be since the last time a frame fired by increment of our update interval. This way code using something
         // like setTimeout() or handling events will get a time that's only updating at around 60fps, or less if
         // we're throttled.
-        auto lastAnimationSchedulerTimestamp = m_document->animationScheduler().lastTimestamp();
+        auto lastAnimationSchedulerTimestamp = currentTime;
         auto delta = Seconds(m_document->domWindow()->nowTimestamp()) - lastAnimationSchedulerTimestamp;
         int frames = std::floor(delta.seconds() / animationInterval().seconds());
-        m_cachedCurrentTime = lastAnimationSchedulerTimestamp + Seconds(frames * animationInterval().seconds());
+        cacheCurrentTime(lastAnimationSchedulerTimestamp + Seconds(frames * animationInterval().seconds()));
 #else
-        m_cachedCurrentTime = Seconds(m_document->domWindow()->nowTimestamp());
+        cacheCurrentTime(currentTime);
 #endif
-        // We want to be sure to keep this time cached until we've both finished running JS and finished updating
-        // animations, so we schedule the invalidation task and register a whenIdle callback on the VM, which will
-        // fire syncronously if no JS is running.
-        m_waitingOnVMIdle = true;
-        if (!m_currentTimeClearingTaskQueue.hasPendingTasks())
-            m_currentTimeClearingTaskQueue.enqueueTask(std::bind(&DocumentTimeline::maybeClearCachedCurrentTime, this));
-        m_document->vm().whenIdle([this, protectedThis = makeRefPtr(this)]() {
-            m_waitingOnVMIdle = false;
-            maybeClearCachedCurrentTime();
-        });
     }
     return m_cachedCurrentTime.value() - m_originTime;
 }
 
+void DocumentTimeline::cacheCurrentTime(Seconds newCurrentTime)
+{
+    m_cachedCurrentTime = newCurrentTime;
+    // We want to be sure to keep this time cached until we've both finished running JS and finished updating
+    // animations, so we schedule the invalidation task and register a whenIdle callback on the VM, which will
+    // fire syncronously if no JS is running.
+    m_waitingOnVMIdle = true;
+    if (!m_currentTimeClearingTaskQueue.hasPendingTasks())
+        m_currentTimeClearingTaskQueue.enqueueTask(std::bind(&DocumentTimeline::maybeClearCachedCurrentTime, this));
+    m_document->vm().whenIdle([this, protectedThis = makeRefPtr(this)]() {
+        m_waitingOnVMIdle = false;
+        maybeClearCachedCurrentTime();
+    });
+}
+
 void DocumentTimeline::maybeClearCachedCurrentTime()
 {
     // We want to make sure we only clear the cached current time if we're not currently running
     // JS or waiting on all current animation updating code to have completed. This is so that
     // we're guaranteed to have a consistent current time reported for all work happening in a given
     // JS frame or throughout updating animations in WebCore.
-    if (!m_waitingOnVMIdle && !m_currentTimeClearingTaskQueue.hasPendingTasks())
+    if (!m_isSuspended && !m_waitingOnVMIdle && !m_currentTimeClearingTaskQueue.hasPendingTasks())
         m_cachedCurrentTime = std::nullopt;
 }
 
 void DocumentTimeline::scheduleAnimationResolutionIfNeeded()
 {
-    if (!m_isSuspended && !m_animations.isEmpty())
+    if (!m_isUpdatingAnimations && !m_isSuspended && !m_animations.isEmpty())
         scheduleAnimationResolution();
 }
 
@@ -337,6 +359,7 @@ void DocumentTimeline::scheduleAnimationResolution()
 
 void DocumentTimeline::unscheduleAnimationResolution()
 {
+    m_tickScheduleTimer.stop();
 #if USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
     m_document->animationScheduler().unscheduleWebAnimationsResolution();
 #else
@@ -353,12 +376,16 @@ void DocumentTimeline::animationResolutionTimerFired()
 #endif
 {
     updateAnimationsAndSendEvents();
+    applyPendingAcceleratedAnimations();
+    scheduleNextTick();
 }
 
 void DocumentTimeline::updateAnimationsAndSendEvents()
 {
     m_numberOfAnimationTimelineInvalidationsForTesting++;
 
+    m_isUpdatingAnimations = true;
+
     // https://drafts.csswg.org/web-animations/#update-animations-and-send-events
 
     // 1. Update the current time of all timelines associated with doc passing now as the timestamp.
@@ -422,7 +449,7 @@ void DocumentTimeline::updateAnimationsAndSendEvents()
     for (auto& completedTransition : completedTransitions)
         transitionDidComplete(completedTransition);
 
-    applyPendingAcceleratedAnimations();
+    m_isUpdatingAnimations = false;
 }
 
 void DocumentTimeline::transitionDidComplete(RefPtr<CSSTransition> transition)
@@ -438,6 +465,34 @@ void DocumentTimeline::transitionDidComplete(RefPtr<CSSTransition> transition)
     }
 }
 
+void DocumentTimeline::scheduleNextTick()
+{
+    // There is no tick to schedule if we don't have any relevant animations.
+    if (m_animations.isEmpty())
+        return;
+
+    for (const auto& animation : m_animations) {
+        if (!animation->isRunningAccelerated()) {
+            scheduleAnimationResolutionIfNeeded();
+            return;
+        }
+    }
+
+    Seconds scheduleDelay = Seconds::infinity();
+
+    for (const auto& animation : m_animations) {
+        auto animationTimeToNextRequiredTick = animation->timeToNextTick();
+        if (animationTimeToNextRequiredTick < animationInterval()) {
+            scheduleAnimationResolutionIfNeeded();
+            return;
+        }
+        scheduleDelay = std::min(scheduleDelay, animationTimeToNextRequiredTick);
+    }
+
+    if (scheduleDelay < Seconds::infinity())
+        m_tickScheduleTimer.startOneShot(scheduleDelay);
+}
+
 bool DocumentTimeline::computeExtentOfAnimation(RenderElement& renderer, LayoutRect& bounds) const
 {
     if (!renderer.element())
@@ -540,7 +595,7 @@ void DocumentTimeline::updateListOfElementsWithRunningAcceleratedAnimationsForEl
     auto animations = animationsForElement(element);
     bool runningAnimationsForElementAreAllAccelerated = !animations.isEmpty();
     for (const auto& animation : animations) {
-        if (is<KeyframeEffect>(animation->effect()) && !downcast<KeyframeEffect>(animation->effect())->isRunningAccelerated()) {
+        if (!animation->isRunningAccelerated()) {
             runningAnimationsForElementAreAllAccelerated = false;
             break;
         }
index 8798e17..fa05217 100644 (file)
@@ -85,6 +85,8 @@ public:
 private:
     DocumentTimeline(Document&, Seconds);
 
+    Seconds liveCurrentTime() const;
+    void cacheCurrentTime(Seconds);
     void scheduleAnimationResolutionIfNeeded();
     void scheduleInvalidationTaskIfNeeded();
     void performInvalidationTask();
@@ -96,17 +98,20 @@ private:
     void maybeClearCachedCurrentTime();
     void updateListOfElementsWithRunningAcceleratedAnimationsForElement(Element&);
     void transitionDidComplete(RefPtr<CSSTransition>);
+    void scheduleNextTick();
 
     RefPtr<Document> m_document;
     Seconds m_originTime;
     bool m_isSuspended { false };
     bool m_waitingOnVMIdle { false };
+    bool m_isUpdatingAnimations { false };
     std::optional<Seconds> m_cachedCurrentTime;
     GenericTaskQueue<Timer> m_currentTimeClearingTaskQueue;
     HashSet<RefPtr<WebAnimation>> m_acceleratedAnimationsPendingRunningStateChange;
     Vector<Ref<AnimationPlaybackEvent>> m_pendingAnimationEvents;
     unsigned m_numberOfAnimationTimelineInvalidationsForTesting { 0 };
     HashSet<Element*> m_elementsWithRunningAcceleratedAnimations;
+    Timer m_tickScheduleTimer;
 
 #if !USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
     void animationResolutionTimerFired();
index 10589bb..850d838 100644 (file)
@@ -1094,6 +1094,11 @@ void WebAnimation::runPendingPauseTask()
     invalidateEffect();
 }
 
+bool WebAnimation::isRunningAccelerated() const
+{
+    return is<KeyframeEffect>(m_effect) && downcast<KeyframeEffect>(*m_effect).isRunningAccelerated();
+}
+
 bool WebAnimation::needsTick() const
 {
     return pending() || playState() == PlayState::Running;
@@ -1114,7 +1119,7 @@ void WebAnimation::tick()
 
 void WebAnimation::resolve(RenderStyle& targetStyle)
 {
-    timingDidChange(DidSeek::No, SynchronouslyNotify::Yes);
+    updateFinishedState(DidSeek::No, SynchronouslyNotify::Yes);
     if (m_effect)
         m_effect->apply(targetStyle);
 }
@@ -1194,4 +1199,40 @@ bool WebAnimation::computeRelevance()
     return phase == AnimationEffect::Phase::Before || (phase == AnimationEffect::Phase::Active && playState() != PlayState::Finished);
 }
 
+Seconds WebAnimation::timeToNextTick() const
+{
+    ASSERT(isRunningAccelerated());
+
+    if (pending())
+        return 0_s;
+
+    // If we're not running, there's no telling when we'll end.
+    if (playState() != PlayState::Running)
+        return Seconds::infinity();
+
+    // CSS Animations dispatch events for each iteration, so compute the time until
+    // the end of this iteration. Any other animation only cares about remaning total time.
+    if (isCSSAnimation()) {
+        // If we're actively running, we need the time until the next iteration.
+        if (auto iterationProgress = effect()->simpleIterationProgress())
+            return effect()->iterationDuration() * (1 - iterationProgress.value());
+
+        // Otherwise we're probably in the before phase waiting to reach our start time.
+        if (auto animationCurrentTime = currentTime()) {
+            // If our current time is negative, we need to be scheduled to be resolved at the inverse
+            // of our current time, unless we fill backwards, in which case we want to invalidate as
+            // soon as possible.
+            auto localTime = animationCurrentTime.value();
+            if (localTime < 0_s)
+                return -localTime;
+            if (localTime < effect()->delay())
+                return effect()->delay() - localTime;
+        }
+    } else if (auto animationCurrentTime = currentTime())
+        return effect()->endTime() - animationCurrentTime.value();
+
+    ASSERT_NOT_REACHED();
+    return Seconds::infinity();
+}
+
 } // namespace WebCore
index 5392836..c99dafa 100644 (file)
@@ -104,11 +104,13 @@ public:
 
     virtual bool needsTick() const;
     virtual void tick();
+    Seconds timeToNextTick() const;
     virtual void resolve(RenderStyle&);
     void effectTargetDidChange(Element* previousTarget, Element* newTarget);
     void acceleratedStateDidChange();
     void applyPendingAcceleratedActions();
 
+    bool isRunningAccelerated() const;
     bool isRelevant() const { return m_isRelevant; }
     void effectTimingDidChange();
     void suspendEffectInvalidation();