DOM timer throttling for hidden plugins
[WebKit-https.git] / Source / WebCore / page / DOMTimer.cpp
1 /*
2  * Copyright (C) 2008 Apple 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. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  *
25  */
26
27 #include "config.h"
28 #include "DOMTimer.h"
29
30 #include "HTMLPlugInElement.h"
31 #include "InspectorInstrumentation.h"
32 #include "PluginViewBase.h"
33 #include "ScheduledAction.h"
34 #include "ScriptExecutionContext.h"
35 #include "UserGestureIndicator.h"
36 #include <wtf/CurrentTime.h>
37 #include <wtf/HashSet.h>
38 #include <wtf/StdLibExtras.h>
39
40 #if PLATFORM(IOS)
41 #include "Chrome.h"
42 #include "ChromeClient.h"
43 #include "Frame.h"
44 #include "Page.h"
45 #include "WKContentObservation.h"
46 #endif
47
48 namespace WebCore {
49
50 static const int maxIntervalForUserGestureForwarding = 1000; // One second matches Gecko.
51 static const int minIntervalForNonUserObservablePluginScriptTimers = 1000; // Empirically determined to maximize battery life.
52 static const int maxTimerNestingLevel = 5;
53 static const double oneMillisecond = 0.001;
54
55 struct DOMTimerFireState {
56     DOMTimerFireState(ScriptExecutionContext* context)
57         : scriptDidInteractWithNonUserObservablePlugin(false)
58         , scriptDidInteractWithUserObservablePlugin(false)
59         , shouldSetCurrent(context->isDocument())
60     {
61         // For worker threads, don't update the current DOMTimerFireState.
62         // Setting this from workers would not be thread-safe, and its not relevant to current uses.
63         if (shouldSetCurrent) {
64             previous = current;
65             current = this;
66         }
67     }
68
69     ~DOMTimerFireState()
70     {
71         if (shouldSetCurrent)
72             current = previous;
73     }
74
75     static DOMTimerFireState* current;
76
77     bool scriptDidInteractWithNonUserObservablePlugin;
78     bool scriptDidInteractWithUserObservablePlugin;
79
80 private:
81     bool shouldSetCurrent;
82     DOMTimerFireState* previous;
83 };
84
85 DOMTimerFireState* DOMTimerFireState::current = nullptr;
86
87 static inline bool shouldForwardUserGesture(int interval, int nestingLevel)
88 {
89     return UserGestureIndicator::processingUserGesture()
90         && interval <= maxIntervalForUserGestureForwarding
91         && !nestingLevel; // Gestures should not be forwarded to nested timers.
92 }
93
94 DOMTimer::DOMTimer(ScriptExecutionContext* context, std::unique_ptr<ScheduledAction> action, int interval, bool singleShot)
95     : SuspendableTimer(context)
96     , m_nestingLevel(context->timerNestingLevel())
97     , m_action(WTF::move(action))
98     , m_originalInterval(interval)
99     , m_throttleState(Undetermined)
100     , m_currentTimerInterval(intervalClampedToMinimum())
101     , m_shouldForwardUserGesture(shouldForwardUserGesture(interval, m_nestingLevel))
102 {
103     RefPtr<DOMTimer> reference = adoptRef(this);
104
105     // Keep asking for the next id until we're given one that we don't already have.
106     do {
107         m_timeoutId = context->circularSequentialID();
108     } while (!context->addTimeout(m_timeoutId, reference));
109
110     if (singleShot)
111         startOneShot(m_currentTimerInterval);
112     else
113         startRepeating(m_currentTimerInterval);
114 }
115
116 int DOMTimer::install(ScriptExecutionContext* context, std::unique_ptr<ScheduledAction> action, int timeout, bool singleShot)
117 {
118     // DOMTimer constructor passes ownership of the initial ref on the object to the constructor.
119     // This reference will be released automatically when a one-shot timer fires, when the context
120     // is destroyed, or if explicitly cancelled by removeById. 
121     DOMTimer* timer = new DOMTimer(context, WTF::move(action), timeout, singleShot);
122 #if PLATFORM(IOS)
123     if (context->isDocument()) {
124         Document& document = toDocument(*context);
125         bool didDeferTimeout = document.frame() && document.frame()->timersPaused();
126         if (!didDeferTimeout && timeout <= 100 && singleShot) {
127             WKSetObservedContentChange(WKContentIndeterminateChange);
128             WebThreadAddObservedContentModifier(timer); // Will only take affect if not already visibility change.
129         }
130     }
131 #endif
132
133     timer->suspendIfNeeded();
134     InspectorInstrumentation::didInstallTimer(context, timer->m_timeoutId, timeout, singleShot);
135
136     return timer->m_timeoutId;
137 }
138
139 void DOMTimer::removeById(ScriptExecutionContext* context, int timeoutId)
140 {
141     // timeout IDs have to be positive, and 0 and -1 are unsafe to
142     // even look up since they are the empty and deleted value
143     // respectively
144     if (timeoutId <= 0)
145         return;
146
147     InspectorInstrumentation::didRemoveTimer(context, timeoutId);
148     context->removeTimeout(timeoutId);
149 }
150
151 void DOMTimer::scriptDidInteractWithPlugin(HTMLPlugInElement& pluginElement)
152 {
153     if (!DOMTimerFireState::current)
154         return;
155
156     if (pluginElement.isUserObservable())
157         DOMTimerFireState::current->scriptDidInteractWithUserObservablePlugin = true;
158     else
159         DOMTimerFireState::current->scriptDidInteractWithNonUserObservablePlugin = true;
160 }
161
162 void DOMTimer::fired()
163 {
164     // Retain this - if the timer is cancelled while this function is on the stack (implicitly and always
165     // for one-shot timers, or if removeById is called on itself from within an interval timer fire) then
166     // wait unit the end of this function to delete DOMTimer.
167     RefPtr<DOMTimer> reference = this;
168
169     ScriptExecutionContext* context = scriptExecutionContext();
170     ASSERT(context);
171
172     DOMTimerFireState fireState(context);
173
174 #if PLATFORM(IOS)
175     Document* document = nullptr;
176     if (context->isDocument()) {
177         document = toDocument(context);
178         ASSERT(!document->frame()->timersPaused());
179     }
180 #endif
181     context->setTimerNestingLevel(std::min(m_nestingLevel + 1, maxTimerNestingLevel));
182
183     ASSERT(!isSuspended());
184     ASSERT(!context->activeDOMObjectsAreSuspended());
185     UserGestureIndicator gestureIndicator(m_shouldForwardUserGesture ? DefinitelyProcessingUserGesture : PossiblyProcessingUserGesture);
186     // Only the first execution of a multi-shot timer should get an affirmative user gesture indicator.
187     m_shouldForwardUserGesture = false;
188
189     InspectorInstrumentationCookie cookie = InspectorInstrumentation::willFireTimer(context, m_timeoutId);
190
191     // Simple case for non-one-shot timers.
192     if (isActive()) {
193         if (m_nestingLevel < maxTimerNestingLevel) {
194             m_nestingLevel++;
195             updateTimerIntervalIfNecessary();
196         }
197
198         m_action->execute(context);
199
200         InspectorInstrumentation::didFireTimer(cookie);
201
202         if (fireState.scriptDidInteractWithUserObservablePlugin && m_throttleState != ShouldNotThrottle) {
203             m_throttleState = ShouldNotThrottle;
204             updateTimerIntervalIfNecessary();
205         } else if (fireState.scriptDidInteractWithNonUserObservablePlugin && m_throttleState == Undetermined) {
206             m_throttleState = ShouldThrottle;
207             updateTimerIntervalIfNecessary();
208         }
209
210         return;
211     }
212
213     context->removeTimeout(m_timeoutId);
214
215 #if PLATFORM(IOS)
216     bool shouldReportLackOfChanges;
217     bool shouldBeginObservingChanges;
218     if (document) {
219         shouldReportLackOfChanges = WebThreadCountOfObservedContentModifiers() == 1;
220         shouldBeginObservingChanges = WebThreadContainsObservedContentModifier(this);
221     } else {
222         shouldReportLackOfChanges = false;
223         shouldBeginObservingChanges = false;
224     }
225
226     if (shouldBeginObservingChanges) {
227         WKBeginObservingContentChanges(false);
228         WebThreadRemoveObservedContentModifier(this);
229     }
230 #endif
231
232     m_action->execute(context);
233
234 #if PLATFORM(IOS)
235     if (shouldBeginObservingChanges) {
236         WKStopObservingContentChanges();
237
238         if (WKObservedContentChange() == WKContentVisibilityChange || shouldReportLackOfChanges)
239             if (document && document->page())
240                 document->page()->chrome().client().observedContentChange(document->frame());
241     }
242 #endif
243
244     InspectorInstrumentation::didFireTimer(cookie);
245
246     context->setTimerNestingLevel(0);
247 }
248
249 void DOMTimer::didStop()
250 {
251     // Need to release JS objects potentially protected by ScheduledAction
252     // because they can form circular references back to the ScriptExecutionContext
253     // which will cause a memory leak.
254     m_action = nullptr;
255 }
256
257 void DOMTimer::updateTimerIntervalIfNecessary()
258 {
259     ASSERT(m_nestingLevel <= maxTimerNestingLevel);
260
261     double previousInterval = m_currentTimerInterval;
262     m_currentTimerInterval = intervalClampedToMinimum();
263
264     if (previousInterval == m_currentTimerInterval)
265         return;
266
267     if (repeatInterval()) {
268         ASSERT(repeatInterval() == previousInterval);
269         augmentRepeatInterval(m_currentTimerInterval - previousInterval);
270     } else
271         augmentFireInterval(m_currentTimerInterval - previousInterval);
272 }
273
274 double DOMTimer::intervalClampedToMinimum() const
275 {
276     ASSERT(scriptExecutionContext());
277     ASSERT(m_nestingLevel <= maxTimerNestingLevel);
278
279     double intervalInSeconds = std::max(oneMillisecond, m_originalInterval * oneMillisecond);
280
281     // Only apply throttling to repeating timers.
282     if (m_nestingLevel < maxTimerNestingLevel)
283         return intervalInSeconds;
284
285     // Apply two throttles - the global (per Page) minimum, and also a per-timer throttle.
286     intervalInSeconds = std::max(intervalInSeconds, scriptExecutionContext()->minimumTimerInterval());
287     if (m_throttleState == ShouldThrottle)
288         intervalInSeconds = std::max(intervalInSeconds, minIntervalForNonUserObservablePluginScriptTimers * oneMillisecond);
289     return intervalInSeconds;
290 }
291
292 double DOMTimer::alignedFireTime(double fireTime) const
293 {
294     double alignmentInterval = scriptExecutionContext()->timerAlignmentInterval();
295     if (alignmentInterval) {
296         double currentTime = monotonicallyIncreasingTime();
297         if (fireTime <= currentTime)
298             return fireTime;
299
300         double alignedTime = ceil(fireTime / alignmentInterval) * alignmentInterval;
301         return alignedTime;
302     }
303
304     return fireTime;
305 }
306
307 } // namespace WebCore