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