1/*
2 * Copyright (C) 2008, 2014 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 "Logging.h"
33#include "Page.h"
34#include "PluginViewBase.h"
35#include "ScheduledAction.h"
36#include "ScriptExecutionContext.h"
37#include "Settings.h"
38#include <wtf/HashMap.h>
39#include <wtf/MathExtras.h>
40#include <wtf/NeverDestroyed.h>
41#include <wtf/RandomNumber.h>
42#include <wtf/StdLibExtras.h>
43
44#if PLATFORM(IOS_FAMILY)
45#include "ContentChangeObserver.h"
46#endif
47
48namespace WebCore {
49
50static const Seconds maxIntervalForUserGestureForwarding { 1_s }; // One second matches Gecko.
51static const Seconds minIntervalForNonUserObservableChangeTimers { 1_s }; // Empirically determined to maximize battery life.
52static const int maxTimerNestingLevel = 5;
53
54class DOMTimerFireState {
55public:
56 DOMTimerFireState(ScriptExecutionContext& context, int nestingLevel)
57 : m_context(context)
58 , m_contextIsDocument(is<Document>(m_context))
59 {
60 // For worker threads, don't update the current DOMTimerFireState.
61 // Setting this from workers would not be thread-safe, and its not relevant to current uses.
62 if (m_contextIsDocument) {
63 m_initialDOMTreeVersion = downcast<Document>(context).domTreeVersion();
64 m_previous = current;
65 current = this;
66 }
67
68 m_context.setTimerNestingLevel(nestingLevel);
69 }
70
71 ~DOMTimerFireState()
72 {
73 if (m_contextIsDocument)
74 current = m_previous;
75 m_context.setTimerNestingLevel(0);
76 }
77
78 Document* contextDocument() const { return m_contextIsDocument ? &downcast<Document>(m_context) : nullptr; }
79
80 void setScriptMadeUserObservableChanges() { m_scriptMadeUserObservableChanges = true; }
81 void setScriptMadeNonUserObservableChanges() { m_scriptMadeNonUserObservableChanges = true; }
82
83 bool scriptMadeNonUserObservableChanges() const { return m_scriptMadeNonUserObservableChanges; }
84 bool scriptMadeUserObservableChanges() const
85 {
86 if (m_scriptMadeUserObservableChanges)
87 return true;
88
89 Document* document = contextDocument();
90 // To be conservative, we also consider any DOM Tree change to be user observable.
91 return document && document->domTreeVersion() != m_initialDOMTreeVersion;
92 }
93
94 static DOMTimerFireState* current;
95
96private:
97 ScriptExecutionContext& m_context;
98 uint64_t m_initialDOMTreeVersion;
99 DOMTimerFireState* m_previous;
100 bool m_contextIsDocument;
101 bool m_scriptMadeNonUserObservableChanges { false };
102 bool m_scriptMadeUserObservableChanges { false };
103};
104
105DOMTimerFireState* DOMTimerFireState::current = nullptr;
106
107struct NestedTimersMap {
108 typedef HashMap<int, Ref<DOMTimer>>::const_iterator const_iterator;
109
110 static NestedTimersMap* instanceForContext(ScriptExecutionContext& context)
111 {
112 // For worker threads, we don't use NestedTimersMap as doing so would not
113 // be thread safe.
114 if (is<Document>(context))
115 return &instance();
116 return nullptr;
117 }
118
119 void startTracking()
120 {
121 // Make sure we start with an empty HashMap. In theory, it is possible the HashMap is not
122 // empty if a timer fires during the execution of another timer (may happen with the
123 // in-process Web Inspector).
124 nestedTimers.clear();
125 isTrackingNestedTimers = true;
126 }
127
128 void stopTracking()
129 {
130 isTrackingNestedTimers = false;
131 nestedTimers.clear();
132 }
133
134 void add(int timeoutId, Ref<DOMTimer>&& timer)
135 {
136 if (isTrackingNestedTimers)
137 nestedTimers.add(timeoutId, WTFMove(timer));
138 }
139
140 void remove(int timeoutId)
141 {
142 if (isTrackingNestedTimers)
143 nestedTimers.remove(timeoutId);
144 }
145
146 const_iterator begin() const { return nestedTimers.begin(); }
147 const_iterator end() const { return nestedTimers.end(); }
148
149private:
150 static NestedTimersMap& instance()
151 {
152 static NeverDestroyed<NestedTimersMap> map;
153 return map;
154 }
155
156 static bool isTrackingNestedTimers;
157 HashMap<int /* timeoutId */, Ref<DOMTimer>> nestedTimers;
158};
159
160bool NestedTimersMap::isTrackingNestedTimers = false;
161
162DOMTimer::DOMTimer(ScriptExecutionContext& context, std::unique_ptr<ScheduledAction> action, Seconds interval, bool singleShot)
163 : SuspendableTimer(context)
164 , m_nestingLevel(context.timerNestingLevel())
165 , m_action(WTFMove(action))
166 , m_originalInterval(interval)
167 , m_throttleState(Undetermined)
168 , m_currentTimerInterval(intervalClampedToMinimum())
169 , m_userGestureTokenToForward(UserGestureIndicator::currentUserGesture())
170{
171 RefPtr<DOMTimer> reference = adoptRef(this);
172
173 // Keep asking for the next id until we're given one that we don't already have.
174 do {
175 m_timeoutId = context.circularSequentialID();
176 } while (!context.addTimeout(m_timeoutId, *this));
177
178 if (singleShot)
179 startOneShot(m_currentTimerInterval);
180 else
181 startRepeating(m_currentTimerInterval);
182}
183
184DOMTimer::~DOMTimer() = default;
185
186int DOMTimer::install(ScriptExecutionContext& context, std::unique_ptr<ScheduledAction> action, Seconds timeout, bool singleShot)
187{
188 // DOMTimer constructor passes ownership of the initial ref on the object to the constructor.
189 // This reference will be released automatically when a one-shot timer fires, when the context
190 // is destroyed, or if explicitly cancelled by removeById.
191 DOMTimer* timer = new DOMTimer(context, WTFMove(action), timeout, singleShot);
192 timer->suspendIfNeeded();
193 InspectorInstrumentation::didInstallTimer(context, timer->m_timeoutId, timeout, singleShot);
194
195 // Keep track of nested timer installs.
196 if (NestedTimersMap* nestedTimers = NestedTimersMap::instanceForContext(context))
197 nestedTimers->add(timer->m_timeoutId, *timer);
198#if PLATFORM(IOS_FAMILY)
199 if (is<Document>(context))
200 downcast<Document>(context).contentChangeObserver().didInstallDOMTimer(*timer, timeout, singleShot);
201#endif
202 return timer->m_timeoutId;
203}
204
205void DOMTimer::removeById(ScriptExecutionContext& context, int timeoutId)
206{
207 // timeout IDs have to be positive, and 0 and -1 are unsafe to
208 // even look up since they are the empty and deleted value
209 // respectively
210 if (timeoutId <= 0)
211 return;
212
213#if PLATFORM(IOS_FAMILY)
214 if (is<Document>(context)) {
215 auto& document = downcast<Document>(context);
216 if (auto* timer = document.findTimeout(timeoutId))
217 document.contentChangeObserver().didRemoveDOMTimer(*timer);
218 }
219#endif
220
221 if (NestedTimersMap* nestedTimers = NestedTimersMap::instanceForContext(context))
222 nestedTimers->remove(timeoutId);
223
224 InspectorInstrumentation::didRemoveTimer(context, timeoutId);
225 context.removeTimeout(timeoutId);
226}
227
228inline bool DOMTimer::isDOMTimersThrottlingEnabled(Document& document) const
229{
230 auto* page = document.page();
231 if (!page)
232 return true;
233 return page->settings().domTimersThrottlingEnabled();
234}
235
236void DOMTimer::updateThrottlingStateIfNecessary(const DOMTimerFireState& fireState)
237{
238 Document* contextDocument = fireState.contextDocument();
239 // We don't throttle timers in worker threads.
240 if (!contextDocument)
241 return;
242
243 if (UNLIKELY(!isDOMTimersThrottlingEnabled(*contextDocument))) {
244 if (m_throttleState == ShouldThrottle) {
245 // Unthrottle the timer in case it was throttled before the setting was updated.
246 LOG(DOMTimers, "%p - Unthrottling DOM timer because throttling was disabled via settings.", this);
247 m_throttleState = ShouldNotThrottle;
248 updateTimerIntervalIfNecessary();
249 }
250 return;
251 }
252
253 if (fireState.scriptMadeUserObservableChanges()) {
254 if (m_throttleState != ShouldNotThrottle) {
255 m_throttleState = ShouldNotThrottle;
256 updateTimerIntervalIfNecessary();
257 }
258 } else if (fireState.scriptMadeNonUserObservableChanges()) {
259 if (m_throttleState != ShouldThrottle) {
260 m_throttleState = ShouldThrottle;
261 updateTimerIntervalIfNecessary();
262 }
263 }
264}
265
266void DOMTimer::scriptDidInteractWithPlugin(HTMLPlugInElement& pluginElement)
267{
268 if (!DOMTimerFireState::current)
269 return;
270
271 if (pluginElement.isUserObservable())
272 DOMTimerFireState::current->setScriptMadeUserObservableChanges();
273 else
274 DOMTimerFireState::current->setScriptMadeNonUserObservableChanges();
275}
276
277void DOMTimer::fired()
278{
279 // Retain this - if the timer is cancelled while this function is on the stack (implicitly and always
280 // for one-shot timers, or if removeById is called on itself from within an interval timer fire) then
281 // wait unit the end of this function to delete DOMTimer.
282 RefPtr<DOMTimer> reference = this;
283
284 ASSERT(scriptExecutionContext());
285 ScriptExecutionContext& context = *scriptExecutionContext();
286
287 DOMTimerFireState fireState(context, std::min(m_nestingLevel + 1, maxTimerNestingLevel));
288
289 if (m_userGestureTokenToForward && m_userGestureTokenToForward->hasExpired(maxIntervalForUserGestureForwarding))
290 m_userGestureTokenToForward = nullptr;
291
292 ASSERT(!isSuspended());
293 ASSERT(!context.activeDOMObjectsAreSuspended());
294 UserGestureIndicator gestureIndicator(m_userGestureTokenToForward);
295 // Only the first execution of a multi-shot timer should get an affirmative user gesture indicator.
296 m_userGestureTokenToForward = nullptr;
297
298 InspectorInstrumentationCookie cookie = InspectorInstrumentation::willFireTimer(context, m_timeoutId, !repeatInterval());
299
300 // Simple case for non-one-shot timers.
301 if (isActive()) {
302 if (m_nestingLevel < maxTimerNestingLevel) {
303 m_nestingLevel++;
304 updateTimerIntervalIfNecessary();
305 }
306
307 m_action->execute(context);
308
309 InspectorInstrumentation::didFireTimer(cookie);
310 updateThrottlingStateIfNecessary(fireState);
311
312 return;
313 }
314
315 context.removeTimeout(m_timeoutId);
316
317 // Keep track nested timer installs.
318 NestedTimersMap* nestedTimers = NestedTimersMap::instanceForContext(context);
319 if (nestedTimers)
320 nestedTimers->startTracking();
321
322#if PLATFORM(IOS_FAMILY)
323 ContentChangeObserver::DOMTimerScope observingScope(is<Document>(context) ? &downcast<Document>(context) : nullptr, *this);
324#endif
325 m_action->execute(context);
326
327 InspectorInstrumentation::didFireTimer(cookie);
328
329 // Check if we should throttle nested single-shot timers.
330 if (nestedTimers) {
331 for (auto& idAndTimer : *nestedTimers) {
332 auto& timer = idAndTimer.value;
333 if (timer->isActive() && !timer->repeatInterval())
334 timer->updateThrottlingStateIfNecessary(fireState);
335 }
336 nestedTimers->stopTracking();
337 }
338}
339
340void DOMTimer::didStop()
341{
342 // Need to release JS objects potentially protected by ScheduledAction
343 // because they can form circular references back to the ScriptExecutionContext
344 // which will cause a memory leak.
345 m_action = nullptr;
346}
347
348void DOMTimer::updateTimerIntervalIfNecessary()
349{
350 ASSERT(m_nestingLevel <= maxTimerNestingLevel);
351
352 auto previousInterval = m_currentTimerInterval;
353 m_currentTimerInterval = intervalClampedToMinimum();
354 if (previousInterval == m_currentTimerInterval)
355 return;
356
357 if (repeatInterval()) {
358 ASSERT(repeatInterval() == previousInterval);
359 LOG(DOMTimers, "%p - Updating DOMTimer's repeat interval from %.2f ms to %.2f ms due to throttling.", this, previousInterval.milliseconds(), m_currentTimerInterval.milliseconds());
360 augmentRepeatInterval(m_currentTimerInterval - previousInterval);
361 } else {
362 LOG(DOMTimers, "%p - Updating DOMTimer's fire interval from %.2f ms to %.2f ms due to throttling.", this, previousInterval.milliseconds(), m_currentTimerInterval.milliseconds());
363 augmentFireInterval(m_currentTimerInterval - previousInterval);
364 }
365}
366
367Seconds DOMTimer::intervalClampedToMinimum() const
368{
369 ASSERT(scriptExecutionContext());
370 ASSERT(m_nestingLevel <= maxTimerNestingLevel);
371
372 Seconds interval = std::max(1_ms, m_originalInterval);
373
374 // Only apply throttling to repeating timers.
375 if (m_nestingLevel < maxTimerNestingLevel)
376 return interval;
377
378 // Apply two throttles - the global (per Page) minimum, and also a per-timer throttle.
379 interval = std::max(interval, scriptExecutionContext()->minimumDOMTimerInterval());
380 if (m_throttleState == ShouldThrottle)
381 interval = std::max(interval, minIntervalForNonUserObservableChangeTimers);
382 return interval;
383}
384
385Optional<MonotonicTime> DOMTimer::alignedFireTime(MonotonicTime fireTime) const
386{
387 Seconds alignmentInterval = scriptExecutionContext()->domTimerAlignmentInterval(m_nestingLevel >= maxTimerNestingLevel);
388 if (!alignmentInterval)
389 return WTF::nullopt;
390
391 static const double randomizedProportion = randomNumber();
392
393 // Force alignment to randomizedAlignment fraction of the way between alignemntIntervals, e.g.
394 // if alignmentInterval is 10_ms and randomizedAlignment is 0.3 this will align to 3, 13, 23, ...
395 Seconds randomizedOffset = alignmentInterval * randomizedProportion;
396 MonotonicTime adjustedFireTime = fireTime - randomizedOffset;
397 return adjustedFireTime - (adjustedFireTime % alignmentInterval) + alignmentInterval + randomizedOffset;
398}
399
400const char* DOMTimer::activeDOMObjectName() const
401{
402 return "DOMTimer";
403}
404
405} // namespace WebCore
406