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