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