1/*
2 * Copyright (C) 2011 Google Inc. All Rights Reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16 * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
17 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
20 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23 *
24 */
25
26#include "config.h"
27#include "ScriptedAnimationController.h"
28
29#include "Chrome.h"
30#include "ChromeClient.h"
31#include "CustomHeaderFields.h"
32#include "DOMWindow.h"
33#include "Document.h"
34#include "DocumentLoader.h"
35#include "Frame.h"
36#include "FrameView.h"
37#include "InspectorInstrumentation.h"
38#include "Logging.h"
39#include "Page.h"
40#include "RequestAnimationFrameCallback.h"
41#include "Settings.h"
42#include <algorithm>
43#include <wtf/Ref.h>
44#include <wtf/SystemTracing.h>
45#include <wtf/text/StringBuilder.h>
46
47// Allow a little more than 60fps to make sure we can at least hit that frame rate.
48static const Seconds fullSpeedAnimationInterval { 15_ms };
49// Allow a little more than 30fps to make sure we can at least hit that frame rate.
50static const Seconds halfSpeedThrottlingAnimationInterval { 30_ms };
51static const Seconds aggressiveThrottlingAnimationInterval { 10_s };
52
53#define RELEASE_LOG_IF_ALLOWED(fmt, ...) RELEASE_LOG_IF(page() && page()->isAlwaysOnLoggingAllowed(), PerformanceLogging, "%p - ScriptedAnimationController::" fmt, this, ##__VA_ARGS__)
54
55namespace WebCore {
56
57ScriptedAnimationController::ScriptedAnimationController(Document& document)
58 : m_document(makeWeakPtr(document))
59 , m_animationTimer(*this, &ScriptedAnimationController::animationTimerFired)
60{
61}
62
63ScriptedAnimationController::~ScriptedAnimationController() = default;
64
65bool ScriptedAnimationController::requestAnimationFrameEnabled() const
66{
67 return m_document && m_document->settings().requestAnimationFrameEnabled();
68}
69
70void ScriptedAnimationController::suspend()
71{
72 ++m_suspendCount;
73}
74
75void ScriptedAnimationController::resume()
76{
77 // It would be nice to put an ASSERT(m_suspendCount > 0) here, but in WK1 resume() can be called
78 // even when suspend hasn't (if a tab was created in the background).
79 if (m_suspendCount > 0)
80 --m_suspendCount;
81
82 if (!m_suspendCount && m_callbacks.size())
83 scheduleAnimation();
84}
85
86#if USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR) && !RELEASE_LOG_DISABLED
87
88static const char* throttlingReasonToString(ScriptedAnimationController::ThrottlingReason reason)
89{
90 switch (reason) {
91 case ScriptedAnimationController::ThrottlingReason::VisuallyIdle:
92 return "VisuallyIdle";
93 case ScriptedAnimationController::ThrottlingReason::OutsideViewport:
94 return "OutsideViewport";
95 case ScriptedAnimationController::ThrottlingReason::LowPowerMode:
96 return "LowPowerMode";
97 case ScriptedAnimationController::ThrottlingReason::NonInteractedCrossOriginFrame:
98 return "NonInteractiveCrossOriginFrame";
99 }
100}
101
102static String throttlingReasonsToString(OptionSet<ScriptedAnimationController::ThrottlingReason> reasons)
103{
104 if (reasons.isEmpty())
105 return "[Unthrottled]"_s;
106
107 StringBuilder builder;
108 for (auto reason : reasons) {
109 if (!builder.isEmpty())
110 builder.append('|');
111 builder.append(throttlingReasonToString(reason));
112 }
113 return builder.toString();
114}
115
116#endif
117
118void ScriptedAnimationController::addThrottlingReason(ThrottlingReason reason)
119{
120#if USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
121 if (m_throttlingReasons.contains(reason))
122 return;
123
124 m_throttlingReasons.add(reason);
125
126 RELEASE_LOG_IF_ALLOWED("addThrottlingReason(%s) -> %s", throttlingReasonToString(reason), throttlingReasonsToString(m_throttlingReasons).utf8().data());
127
128 if (m_animationTimer.isActive()) {
129 m_animationTimer.stop();
130 scheduleAnimation();
131 }
132#else
133 UNUSED_PARAM(reason);
134#endif
135}
136
137void ScriptedAnimationController::removeThrottlingReason(ThrottlingReason reason)
138{
139#if USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
140 if (!m_throttlingReasons.contains(reason))
141 return;
142
143 m_throttlingReasons.remove(reason);
144
145 RELEASE_LOG_IF_ALLOWED("removeThrottlingReason(%s) -> %s", throttlingReasonToString(reason), throttlingReasonsToString(m_throttlingReasons).utf8().data());
146
147 if (m_animationTimer.isActive()) {
148 m_animationTimer.stop();
149 scheduleAnimation();
150 }
151#else
152 UNUSED_PARAM(reason);
153#endif
154}
155
156bool ScriptedAnimationController::isThrottled() const
157{
158#if USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
159 return !m_throttlingReasons.isEmpty();
160#else
161 return false;
162#endif
163}
164
165ScriptedAnimationController::CallbackId ScriptedAnimationController::registerCallback(Ref<RequestAnimationFrameCallback>&& callback)
166{
167 ScriptedAnimationController::CallbackId id = ++m_nextCallbackId;
168 callback->m_firedOrCancelled = false;
169 callback->m_id = id;
170 m_callbacks.append(WTFMove(callback));
171
172 if (m_document)
173 InspectorInstrumentation::didRequestAnimationFrame(*m_document, id);
174
175 if (!m_suspendCount)
176 scheduleAnimation();
177 return id;
178}
179
180void ScriptedAnimationController::cancelCallback(CallbackId id)
181{
182 for (size_t i = 0; i < m_callbacks.size(); ++i) {
183 if (m_callbacks[i]->m_id == id) {
184 m_callbacks[i]->m_firedOrCancelled = true;
185 InspectorInstrumentation::didCancelAnimationFrame(*m_document, id);
186 m_callbacks.remove(i);
187 return;
188 }
189 }
190}
191
192void ScriptedAnimationController::serviceRequestAnimationFrameCallbacks(DOMHighResTimeStamp timestamp)
193{
194 if (!m_callbacks.size() || m_suspendCount || !requestAnimationFrameEnabled())
195 return;
196
197 TraceScope tracingScope(RAFCallbackStart, RAFCallbackEnd);
198
199 // We round this to the nearest microsecond so that we can return a time that matches what is returned by document.timeline.currentTime.
200 DOMHighResTimeStamp highResNowMs = std::round(1000 * timestamp);
201
202 // First, generate a list of callbacks to consider. Callbacks registered from this point
203 // on are considered only for the "next" frame, not this one.
204 CallbackList callbacks(m_callbacks);
205
206 // Invoking callbacks may detach elements from our document, which clears the document's
207 // reference to us, so take a defensive reference.
208 Ref<ScriptedAnimationController> protectedThis(*this);
209 Ref<Document> protectedDocument(*m_document);
210
211 for (auto& callback : callbacks) {
212 if (callback->m_firedOrCancelled)
213 continue;
214 callback->m_firedOrCancelled = true;
215 InspectorInstrumentationCookie cookie = InspectorInstrumentation::willFireAnimationFrame(protectedDocument, callback->m_id);
216 callback->handleEvent(highResNowMs);
217 InspectorInstrumentation::didFireAnimationFrame(cookie);
218 }
219
220 // Remove any callbacks we fired from the list of pending callbacks.
221 m_callbacks.removeAllMatching([](auto& callback) {
222 return callback->m_firedOrCancelled;
223 });
224
225 if (m_callbacks.size())
226 scheduleAnimation();
227}
228
229Seconds ScriptedAnimationController::interval() const
230{
231#if USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
232 if (m_throttlingReasons.contains(ThrottlingReason::VisuallyIdle) || m_throttlingReasons.contains(ThrottlingReason::OutsideViewport))
233 return aggressiveThrottlingAnimationInterval;
234
235 if (m_throttlingReasons.contains(ThrottlingReason::LowPowerMode))
236 return halfSpeedThrottlingAnimationInterval;
237
238 if (m_throttlingReasons.contains(ThrottlingReason::NonInteractedCrossOriginFrame))
239 return halfSpeedThrottlingAnimationInterval;
240
241 ASSERT(m_throttlingReasons.isEmpty());
242#endif
243 return fullSpeedAnimationInterval;
244}
245
246Page* ScriptedAnimationController::page() const
247{
248 return m_document ? m_document->page() : nullptr;
249}
250
251void ScriptedAnimationController::scheduleAnimation()
252{
253 if (!requestAnimationFrameEnabled())
254 return;
255
256#if USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
257 if (!m_isUsingTimer && !isThrottled()) {
258 if (auto* page = this->page()) {
259 page->renderingUpdateScheduler().scheduleTimedRenderingUpdate();
260 return;
261 }
262
263 m_isUsingTimer = true;
264 }
265#endif
266 if (m_animationTimer.isActive())
267 return;
268
269 Seconds animationInterval = interval();
270 Seconds scheduleDelay = std::max(animationInterval - Seconds(m_document->domWindow()->nowTimestamp() - m_lastAnimationFrameTimestamp), 0_s);
271
272 if (isThrottled()) {
273 // FIXME: not ideal to snapshot time both in now() and nowTimestamp(), the latter of which also has reduced resolution.
274 MonotonicTime now = MonotonicTime::now();
275
276 MonotonicTime fireTime = now + scheduleDelay;
277 Seconds alignmentInterval = 10_ms;
278 // Snap to the nearest alignmentInterval.
279 Seconds alignment = (fireTime + alignmentInterval / 2) % alignmentInterval;
280 MonotonicTime alignedFireTime = fireTime - alignment;
281 scheduleDelay = alignedFireTime - now;
282 }
283
284 m_animationTimer.startOneShot(scheduleDelay);
285}
286
287void ScriptedAnimationController::animationTimerFired()
288{
289 m_lastAnimationFrameTimestamp = m_document->domWindow()->nowTimestamp();
290 serviceRequestAnimationFrameCallbacks(m_lastAnimationFrameTimestamp);
291}
292
293}
294