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 "DOMWindow.h"
32#include "Document.h"
33#include "DocumentLoader.h"
34#include "Frame.h"
35#include "FrameView.h"
36#include "InspectorInstrumentation.h"
37#include "Logging.h"
38#include "Page.h"
39#include "RequestAnimationFrameCallback.h"
40#include "Settings.h"
41#include <algorithm>
42#include <wtf/Ref.h>
43#include <wtf/SystemTracing.h>
44#include <wtf/text/StringBuilder.h>
45
46// Allow a little more than 60fps to make sure we can at least hit that frame rate.
47static const Seconds fullSpeedAnimationInterval { 15_ms };
48// Allow a little more than 30fps to make sure we can at least hit that frame rate.
49static const Seconds halfSpeedThrottlingAnimationInterval { 30_ms };
50static const Seconds aggressiveThrottlingAnimationInterval { 10_s };
51
52#define RELEASE_LOG_IF_ALLOWED(fmt, ...) RELEASE_LOG_IF(page() && page()->isAlwaysOnLoggingAllowed(), PerformanceLogging, "%p - ScriptedAnimationController::" fmt, this, ##__VA_ARGS__)
53
54namespace WebCore {
55
56ScriptedAnimationController::ScriptedAnimationController(Document& document)
57 : m_document(makeWeakPtr(document))
58 , m_animationTimer(*this, &ScriptedAnimationController::animationTimerFired)
59{
60}
61
62ScriptedAnimationController::~ScriptedAnimationController() = default;
63
64bool ScriptedAnimationController::requestAnimationFrameEnabled() const
65{
66 return m_document && m_document->settings().requestAnimationFrameEnabled();
67}
68
69void ScriptedAnimationController::suspend()
70{
71 ++m_suspendCount;
72}
73
74void ScriptedAnimationController::resume()
75{
76 // It would be nice to put an ASSERT(m_suspendCount > 0) here, but in WK1 resume() can be called
77 // even when suspend hasn't (if a tab was created in the background).
78 if (m_suspendCount > 0)
79 --m_suspendCount;
80
81 if (!m_suspendCount && m_callbacks.size())
82 scheduleAnimation();
83}
84
85#if USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR) && !RELEASE_LOG_DISABLED
86
87static const char* throttlingReasonToString(ScriptedAnimationController::ThrottlingReason reason)
88{
89 switch (reason) {
90 case ScriptedAnimationController::ThrottlingReason::VisuallyIdle:
91 return "VisuallyIdle";
92 case ScriptedAnimationController::ThrottlingReason::OutsideViewport:
93 return "OutsideViewport";
94 case ScriptedAnimationController::ThrottlingReason::LowPowerMode:
95 return "LowPowerMode";
96 case ScriptedAnimationController::ThrottlingReason::NonInteractedCrossOriginFrame:
97 return "NonInteractiveCrossOriginFrame";
98 }
99}
100
101static String throttlingReasonsToString(OptionSet<ScriptedAnimationController::ThrottlingReason> reasons)
102{
103 if (reasons.isEmpty())
104 return "[Unthrottled]"_s;
105
106 StringBuilder builder;
107 for (auto reason : reasons) {
108 if (!builder.isEmpty())
109 builder.append('|');
110 builder.append(throttlingReasonToString(reason));
111 }
112 return builder.toString();
113}
114
115#endif
116
117void ScriptedAnimationController::addThrottlingReason(ThrottlingReason reason)
118{
119#if USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
120 if (m_throttlingReasons.contains(reason))
121 return;
122
123 m_throttlingReasons.add(reason);
124
125 RELEASE_LOG_IF_ALLOWED("addThrottlingReason(%s) -> %s", throttlingReasonToString(reason), throttlingReasonsToString(m_throttlingReasons).utf8().data());
126
127 if (m_animationTimer.isActive()) {
128 m_animationTimer.stop();
129 scheduleAnimation();
130 }
131#else
132 UNUSED_PARAM(reason);
133#endif
134}
135
136void ScriptedAnimationController::removeThrottlingReason(ThrottlingReason reason)
137{
138#if USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
139 if (!m_throttlingReasons.contains(reason))
140 return;
141
142 m_throttlingReasons.remove(reason);
143
144 RELEASE_LOG_IF_ALLOWED("removeThrottlingReason(%s) -> %s", throttlingReasonToString(reason), throttlingReasonsToString(m_throttlingReasons).utf8().data());
145
146 if (m_animationTimer.isActive()) {
147 m_animationTimer.stop();
148 scheduleAnimation();
149 }
150#else
151 UNUSED_PARAM(reason);
152#endif
153}
154
155bool ScriptedAnimationController::isThrottled() const
156{
157#if USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
158 return !m_throttlingReasons.isEmpty();
159#else
160 return false;
161#endif
162}
163
164ScriptedAnimationController::CallbackId ScriptedAnimationController::registerCallback(Ref<RequestAnimationFrameCallback>&& callback)
165{
166 ScriptedAnimationController::CallbackId id = ++m_nextCallbackId;
167 callback->m_firedOrCancelled = false;
168 callback->m_id = id;
169 m_callbacks.append(WTFMove(callback));
170
171 if (m_document)
172 InspectorInstrumentation::didRequestAnimationFrame(*m_document, id);
173
174 if (!m_suspendCount)
175 scheduleAnimation();
176 return id;
177}
178
179void ScriptedAnimationController::cancelCallback(CallbackId id)
180{
181 for (size_t i = 0; i < m_callbacks.size(); ++i) {
182 if (m_callbacks[i]->m_id == id) {
183 m_callbacks[i]->m_firedOrCancelled = true;
184 InspectorInstrumentation::didCancelAnimationFrame(*m_document, id);
185 m_callbacks.remove(i);
186 return;
187 }
188 }
189}
190
191void ScriptedAnimationController::serviceRequestAnimationFrameCallbacks(DOMHighResTimeStamp timestamp)
192{
193 if (!m_callbacks.size() || m_suspendCount || !requestAnimationFrameEnabled())
194 return;
195
196 TraceScope tracingScope(RAFCallbackStart, RAFCallbackEnd);
197
198 // We round this to the nearest microsecond so that we can return a time that matches what is returned by document.timeline.currentTime.
199 DOMHighResTimeStamp highResNowMs = std::round(1000 * timestamp);
200
201 // First, generate a list of callbacks to consider. Callbacks registered from this point
202 // on are considered only for the "next" frame, not this one.
203 CallbackList callbacks(m_callbacks);
204
205 // Invoking callbacks may detach elements from our document, which clears the document's
206 // reference to us, so take a defensive reference.
207 Ref<ScriptedAnimationController> protectedThis(*this);
208 Ref<Document> protectedDocument(*m_document);
209
210 for (auto& callback : callbacks) {
211 if (callback->m_firedOrCancelled)
212 continue;
213 callback->m_firedOrCancelled = true;
214 InspectorInstrumentationCookie cookie = InspectorInstrumentation::willFireAnimationFrame(protectedDocument, callback->m_id);
215 callback->handleEvent(highResNowMs);
216 InspectorInstrumentation::didFireAnimationFrame(cookie);
217 }
218
219 // Remove any callbacks we fired from the list of pending callbacks.
220 m_callbacks.removeAllMatching([](auto& callback) {
221 return callback->m_firedOrCancelled;
222 });
223
224 if (m_callbacks.size())
225 scheduleAnimation();
226}
227
228Seconds ScriptedAnimationController::interval() const
229{
230#if USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
231 if (m_throttlingReasons.contains(ThrottlingReason::VisuallyIdle) || m_throttlingReasons.contains(ThrottlingReason::OutsideViewport))
232 return aggressiveThrottlingAnimationInterval;
233
234 if (m_throttlingReasons.contains(ThrottlingReason::LowPowerMode))
235 return halfSpeedThrottlingAnimationInterval;
236
237 if (m_throttlingReasons.contains(ThrottlingReason::NonInteractedCrossOriginFrame))
238 return halfSpeedThrottlingAnimationInterval;
239
240 ASSERT(m_throttlingReasons.isEmpty());
241#endif
242 return fullSpeedAnimationInterval;
243}
244
245Page* ScriptedAnimationController::page() const
246{
247 return m_document ? m_document->page() : nullptr;
248}
249
250void ScriptedAnimationController::scheduleAnimation()
251{
252 if (!requestAnimationFrameEnabled())
253 return;
254
255#if USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
256 if (!m_isUsingTimer && !isThrottled()) {
257 if (auto* page = this->page()) {
258 page->renderingUpdateScheduler().scheduleRenderingUpdate();
259 return;
260 }
261
262 m_isUsingTimer = true;
263 }
264#endif
265 if (m_animationTimer.isActive())
266 return;
267
268 Seconds animationInterval = interval();
269 Seconds scheduleDelay = std::max(animationInterval - Seconds(m_document->domWindow()->nowTimestamp() - m_lastAnimationFrameTimestamp), 0_s);
270
271 if (isThrottled()) {
272 // FIXME: not ideal to snapshot time both in now() and nowTimestamp(), the latter of which also has reduced resolution.
273 MonotonicTime now = MonotonicTime::now();
274
275 MonotonicTime fireTime = now + scheduleDelay;
276 Seconds alignmentInterval = 10_ms;
277 // Snap to the nearest alignmentInterval.
278 Seconds alignment = (fireTime + alignmentInterval / 2) % alignmentInterval;
279 MonotonicTime alignedFireTime = fireTime - alignment;
280 scheduleDelay = alignedFireTime - now;
281 }
282
283 m_animationTimer.startOneShot(scheduleDelay);
284}
285
286void ScriptedAnimationController::animationTimerFired()
287{
288 m_lastAnimationFrameTimestamp = m_document->domWindow()->nowTimestamp();
289 serviceRequestAnimationFrameCallbacks(m_lastAnimationFrameTimestamp);
290}
291
292}
293