1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "TimeoutManager.h"
8 #include "nsGlobalWindow.h"
9 #include "mozilla/Logging.h"
10 #include "mozilla/PerformanceCounter.h"
11 #include "mozilla/ProfilerMarkers.h"
12 #include "mozilla/ScopeExit.h"
13 #include "mozilla/StaticPrefs_dom.h"
14 #include "mozilla/StaticPrefs_privacy.h"
15 #include "mozilla/Telemetry.h"
16 #include "mozilla/ThrottledEventQueue.h"
17 #include "mozilla/TimeStamp.h"
18 #include "nsINamed.h"
19 #include "mozilla/dom/DocGroup.h"
20 #include "mozilla/dom/Document.h"
21 #include "mozilla/dom/PopupBlocker.h"
22 #include "mozilla/dom/ContentChild.h"
23 #include "mozilla/dom/TimeoutHandler.h"
24 #include "TimeoutExecutor.h"
25 #include "TimeoutBudgetManager.h"
26 #include "mozilla/net/WebSocketEventService.h"
27 #include "mozilla/MediaManager.h"
28 
29 using namespace mozilla;
30 using namespace mozilla::dom;
31 
32 LazyLogModule gTimeoutLog("Timeout");
33 
34 static int32_t gRunningTimeoutDepth = 0;
35 
36 // static
37 const uint32_t TimeoutManager::InvalidFiringId = 0;
38 
39 namespace {
GetRegenerationFactor(bool aIsBackground)40 double GetRegenerationFactor(bool aIsBackground) {
41   // Lookup function for "dom.timeout.{background,
42   // foreground}_budget_regeneration_rate".
43 
44   // Returns the rate of regeneration of the execution budget as a
45   // fraction. If the value is 1.0, the amount of time regenerated is
46   // equal to time passed. At this rate we regenerate 1ms/ms. If it is
47   // 0.01 the amount regenerated is 1% of time passed. At this rate we
48   // regenerate 1ms/100ms, etc.
49   double denominator = std::max(
50       aIsBackground
51           ? StaticPrefs::dom_timeout_background_budget_regeneration_rate()
52           : StaticPrefs::dom_timeout_foreground_budget_regeneration_rate(),
53       1);
54   return 1.0 / denominator;
55 }
56 
GetMaxBudget(bool aIsBackground)57 TimeDuration GetMaxBudget(bool aIsBackground) {
58   // Lookup function for "dom.timeout.{background,
59   // foreground}_throttling_max_budget".
60 
61   // Returns how high a budget can be regenerated before being
62   // clamped. If this value is less or equal to zero,
63   // TimeDuration::Forever() is implied.
64   int32_t maxBudget =
65       aIsBackground
66           ? StaticPrefs::dom_timeout_background_throttling_max_budget()
67           : StaticPrefs::dom_timeout_foreground_throttling_max_budget();
68   return maxBudget > 0 ? TimeDuration::FromMilliseconds(maxBudget)
69                        : TimeDuration::Forever();
70 }
71 
GetMinBudget(bool aIsBackground)72 TimeDuration GetMinBudget(bool aIsBackground) {
73   // The minimum budget is computed by looking up the maximum allowed
74   // delay and computing how long time it would take to regenerate
75   // that budget using the regeneration factor. This number is
76   // expected to be negative.
77   return TimeDuration::FromMilliseconds(
78       -StaticPrefs::dom_timeout_budget_throttling_max_delay() /
79       std::max(
80           aIsBackground
81               ? StaticPrefs::dom_timeout_background_budget_regeneration_rate()
82               : StaticPrefs::dom_timeout_foreground_budget_regeneration_rate(),
83           1));
84 }
85 }  // namespace
86 
87 //
88 
IsBackground() const89 bool TimeoutManager::IsBackground() const {
90   return !IsActive() && mWindow.IsBackgroundInternal();
91 }
92 
IsActive() const93 bool TimeoutManager::IsActive() const {
94   // A window is considered active if:
95   // * It is a chrome window
96   // * It is playing audio
97   //
98   // Note that a window can be considered active if it is either in the
99   // foreground or in the background.
100 
101   if (mWindow.IsChromeWindow()) {
102     return true;
103   }
104 
105   // Check if we're playing audio
106   if (mWindow.IsPlayingAudio()) {
107     return true;
108   }
109 
110   return false;
111 }
112 
SetLoading(bool value)113 void TimeoutManager::SetLoading(bool value) {
114   // When moving from loading to non-loading, we may need to
115   // reschedule any existing timeouts from the idle timeout queue
116   // to the normal queue.
117   MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("%p: SetLoading(%d)", this, value));
118   if (mIsLoading && !value) {
119     MoveIdleToActive();
120   }
121   // We don't immediately move existing timeouts to the idle queue if we
122   // move to loading.  When they would have fired, we'll see we're loading
123   // and move them then.
124   mIsLoading = value;
125 }
126 
MoveIdleToActive()127 void TimeoutManager::MoveIdleToActive() {
128   uint32_t num = 0;
129   TimeStamp when;
130   TimeStamp now;
131   // Ensure we maintain the ordering of timeouts, so timeouts
132   // never fire before a timeout set for an earlier time, or
133   // before a timeout for the same time already submitted.
134   // See https://html.spec.whatwg.org/#dom-settimeout #16 and #17
135   while (RefPtr<Timeout> timeout = mIdleTimeouts.GetLast()) {
136     if (num == 0) {
137       when = timeout->When();
138     }
139     timeout->remove();
140     mTimeouts.InsertFront(timeout);
141     if (profiler_can_accept_markers()) {
142       if (num == 0) {
143         now = TimeStamp::Now();
144       }
145       TimeDuration elapsed = now - timeout->SubmitTime();
146       TimeDuration target = timeout->When() - timeout->SubmitTime();
147       TimeDuration delta = now - timeout->When();
148       nsPrintfCString marker(
149           "Releasing deferred setTimeout() for %dms (original target time was "
150           "%dms (%dms delta))",
151           int(elapsed.ToMilliseconds()), int(target.ToMilliseconds()),
152           int(delta.ToMilliseconds()));
153       // don't have end before start...
154       PROFILER_MARKER_TEXT(
155           "setTimeout deferred release", DOM,
156           MarkerOptions(
157               MarkerTiming::Interval(
158                   delta.ToMilliseconds() >= 0 ? timeout->When() : now, now),
159               MarkerInnerWindowId(mWindow.WindowID())),
160           marker);
161     }
162     num++;
163   }
164   if (num > 0) {
165     MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(when));
166     mIdleExecutor->Cancel();
167   }
168   MOZ_LOG(gTimeoutLog, LogLevel::Debug,
169           ("%p: Moved %d timeouts from Idle to active", this, num));
170 }
171 
CreateFiringId()172 uint32_t TimeoutManager::CreateFiringId() {
173   uint32_t id = mNextFiringId;
174   mNextFiringId += 1;
175   if (mNextFiringId == InvalidFiringId) {
176     mNextFiringId += 1;
177   }
178 
179   mFiringIdStack.AppendElement(id);
180 
181   return id;
182 }
183 
DestroyFiringId(uint32_t aFiringId)184 void TimeoutManager::DestroyFiringId(uint32_t aFiringId) {
185   MOZ_DIAGNOSTIC_ASSERT(!mFiringIdStack.IsEmpty());
186   MOZ_DIAGNOSTIC_ASSERT(mFiringIdStack.LastElement() == aFiringId);
187   mFiringIdStack.RemoveLastElement();
188 }
189 
IsValidFiringId(uint32_t aFiringId) const190 bool TimeoutManager::IsValidFiringId(uint32_t aFiringId) const {
191   return !IsInvalidFiringId(aFiringId);
192 }
193 
MinSchedulingDelay() const194 TimeDuration TimeoutManager::MinSchedulingDelay() const {
195   if (IsActive()) {
196     return TimeDuration();
197   }
198 
199   bool isBackground = mWindow.IsBackgroundInternal();
200 
201   // If a window isn't active as defined by TimeoutManager::IsActive()
202   // and we're throttling timeouts using an execution budget, we
203   // should adjust the minimum scheduling delay if we have used up all
204   // of our execution budget. Note that a window can be active or
205   // inactive regardless of wether it is in the foreground or in the
206   // background. Throttling using a budget depends largely on the
207   // regeneration factor, which can be specified separately for
208   // foreground and background windows.
209   //
210   // The value that we compute is the time in the future when we again
211   // have a positive execution budget. We do this by taking the
212   // execution budget into account, which if it positive implies that
213   // we have time left to execute, and if it is negative implies that
214   // we should throttle it until the budget again is positive. The
215   // factor used is the rate of budget regeneration.
216   //
217   // We clamp the delay to be less than or equal to
218   // "dom.timeout.budget_throttling_max_delay" to not entirely starve
219   // the timeouts.
220   //
221   // Consider these examples assuming we should throttle using
222   // budgets:
223   //
224   // mExecutionBudget is 20ms
225   // factor is 1, which is 1 ms/ms
226   // delay is 0ms
227   // then we will compute the minimum delay:
228   // max(0, - 20 * 1) = 0
229   //
230   // mExecutionBudget is -50ms
231   // factor is 0.1, which is 1 ms/10ms
232   // delay is 1000ms
233   // then we will compute the minimum delay:
234   // max(1000, - (- 50) * 1/0.1) = max(1000, 500) = 1000
235   //
236   // mExecutionBudget is -15ms
237   // factor is 0.01, which is 1 ms/100ms
238   // delay is 1000ms
239   // then we will compute the minimum delay:
240   // max(1000, - (- 15) * 1/0.01) = max(1000, 1500) = 1500
241   TimeDuration unthrottled =
242       isBackground ? TimeDuration::FromMilliseconds(
243                          StaticPrefs::dom_min_background_timeout_value())
244                    : TimeDuration();
245   bool budgetThrottlingEnabled = BudgetThrottlingEnabled(isBackground);
246   if (budgetThrottlingEnabled && mExecutionBudget < TimeDuration()) {
247     // Only throttle if execution budget is less than 0
248     double factor = 1.0 / GetRegenerationFactor(mWindow.IsBackgroundInternal());
249     return TimeDuration::Max(unthrottled, -mExecutionBudget.MultDouble(factor));
250   }
251   if (!budgetThrottlingEnabled && isBackground) {
252     return TimeDuration::FromMilliseconds(
253         StaticPrefs::
254             dom_min_background_timeout_value_without_budget_throttling());
255   }
256 
257   return unthrottled;
258 }
259 
MaybeSchedule(const TimeStamp & aWhen,const TimeStamp & aNow)260 nsresult TimeoutManager::MaybeSchedule(const TimeStamp& aWhen,
261                                        const TimeStamp& aNow) {
262   MOZ_DIAGNOSTIC_ASSERT(mExecutor);
263 
264   // Before we can schedule the executor we need to make sure that we
265   // have an updated execution budget.
266   UpdateBudget(aNow);
267   return mExecutor->MaybeSchedule(aWhen, MinSchedulingDelay());
268 }
269 
IsInvalidFiringId(uint32_t aFiringId) const270 bool TimeoutManager::IsInvalidFiringId(uint32_t aFiringId) const {
271   // Check the most common ways to invalidate a firing id first.
272   // These should be quite fast.
273   if (aFiringId == InvalidFiringId || mFiringIdStack.IsEmpty()) {
274     return true;
275   }
276 
277   if (mFiringIdStack.Length() == 1) {
278     return mFiringIdStack[0] != aFiringId;
279   }
280 
281   // Next do a range check on the first and last items in the stack
282   // of active firing ids.  This is a bit slower.
283   uint32_t low = mFiringIdStack[0];
284   uint32_t high = mFiringIdStack.LastElement();
285   MOZ_DIAGNOSTIC_ASSERT(low != high);
286   if (low > high) {
287     // If the first element is bigger than the last element in the
288     // stack, that means mNextFiringId wrapped around to zero at
289     // some point.
290     std::swap(low, high);
291   }
292   MOZ_DIAGNOSTIC_ASSERT(low < high);
293 
294   if (aFiringId < low || aFiringId > high) {
295     return true;
296   }
297 
298   // Finally, fall back to verifying the firing id is not anywhere
299   // in the stack.  This could be slow for a large stack, but that
300   // should be rare.  It can only happen with deeply nested event
301   // loop spinning.  For example, a page that does a lot of timers
302   // and a lot of sync XHRs within those timers could be slow here.
303   return !mFiringIdStack.Contains(aFiringId);
304 }
305 
306 // The number of nested timeouts before we start clamping. HTML says 5.
307 #define DOM_CLAMP_TIMEOUT_NESTING_LEVEL 5u
308 
CalculateDelay(Timeout * aTimeout) const309 TimeDuration TimeoutManager::CalculateDelay(Timeout* aTimeout) const {
310   MOZ_DIAGNOSTIC_ASSERT(aTimeout);
311   TimeDuration result = aTimeout->mInterval;
312 
313   if (aTimeout->mNestingLevel >= DOM_CLAMP_TIMEOUT_NESTING_LEVEL) {
314     uint32_t minTimeoutValue = StaticPrefs::dom_min_timeout_value();
315     result = TimeDuration::Max(result,
316                                TimeDuration::FromMilliseconds(minTimeoutValue));
317   }
318 
319   return result;
320 }
321 
GetPerformanceCounter()322 PerformanceCounter* TimeoutManager::GetPerformanceCounter() {
323   Document* doc = mWindow.GetDocument();
324   if (doc) {
325     dom::DocGroup* docGroup = doc->GetDocGroup();
326     if (docGroup) {
327       return docGroup->GetPerformanceCounter();
328     }
329   }
330   return nullptr;
331 }
332 
RecordExecution(Timeout * aRunningTimeout,Timeout * aTimeout)333 void TimeoutManager::RecordExecution(Timeout* aRunningTimeout,
334                                      Timeout* aTimeout) {
335   TimeoutBudgetManager& budgetManager = TimeoutBudgetManager::Get();
336   TimeStamp now = TimeStamp::Now();
337 
338   if (aRunningTimeout) {
339     // If we're running a timeout callback, record any execution until
340     // now.
341     TimeDuration duration = budgetManager.RecordExecution(now, aRunningTimeout);
342 
343     UpdateBudget(now, duration);
344 
345     // This is an ad-hoc way to use the counters for the timers
346     // that should be removed at somepoint. See Bug 1482834
347     PerformanceCounter* counter = GetPerformanceCounter();
348     if (counter) {
349       counter->IncrementExecutionDuration(duration.ToMicroseconds());
350     }
351   }
352 
353   if (aTimeout) {
354     // If we're starting a new timeout callback, start recording.
355     budgetManager.StartRecording(now);
356     PerformanceCounter* counter = GetPerformanceCounter();
357     if (counter) {
358       counter->IncrementDispatchCounter(DispatchCategory(TaskCategory::Timer));
359     }
360   } else {
361     // Else stop by clearing the start timestamp.
362     budgetManager.StopRecording();
363   }
364 }
365 
UpdateBudget(const TimeStamp & aNow,const TimeDuration & aDuration)366 void TimeoutManager::UpdateBudget(const TimeStamp& aNow,
367                                   const TimeDuration& aDuration) {
368   if (mWindow.IsChromeWindow()) {
369     return;
370   }
371 
372   // The budget is adjusted by increasing it with the time since the
373   // last budget update factored with the regeneration rate. If a
374   // runnable has executed, subtract that duration from the
375   // budget. The budget updated without consideration of wether the
376   // window is active or not. If throttling is enabled and the window
377   // is active and then becomes inactive, an overdrawn budget will
378   // still be counted against the minimum delay.
379   bool isBackground = mWindow.IsBackgroundInternal();
380   if (BudgetThrottlingEnabled(isBackground)) {
381     double factor = GetRegenerationFactor(isBackground);
382     TimeDuration regenerated = (aNow - mLastBudgetUpdate).MultDouble(factor);
383     // Clamp the budget to the range of minimum and maximum allowed budget.
384     mExecutionBudget = TimeDuration::Max(
385         GetMinBudget(isBackground),
386         TimeDuration::Min(GetMaxBudget(isBackground),
387                           mExecutionBudget - aDuration + regenerated));
388   } else {
389     // If budget throttling isn't enabled, reset the execution budget
390     // to the max budget specified in preferences. Always doing this
391     // will catch the case of BudgetThrottlingEnabled going from
392     // returning true to returning false. This prevent us from looping
393     // in RunTimeout, due to totalTimeLimit being set to zero and no
394     // timeouts being executed, even though budget throttling isn't
395     // active at the moment.
396     mExecutionBudget = GetMaxBudget(isBackground);
397   }
398 
399   mLastBudgetUpdate = aNow;
400 }
401 
402 // The longest interval (as PRIntervalTime) we permit, or that our
403 // timer code can handle, really. See DELAY_INTERVAL_LIMIT in
404 // nsTimerImpl.h for details.
405 #define DOM_MAX_TIMEOUT_VALUE DELAY_INTERVAL_LIMIT
406 
407 uint32_t TimeoutManager::sNestingLevel = 0;
408 
TimeoutManager(nsGlobalWindowInner & aWindow,uint32_t aMaxIdleDeferMS)409 TimeoutManager::TimeoutManager(nsGlobalWindowInner& aWindow,
410                                uint32_t aMaxIdleDeferMS)
411     : mWindow(aWindow),
412       mExecutor(new TimeoutExecutor(this, false, 0)),
413       mIdleExecutor(new TimeoutExecutor(this, true, aMaxIdleDeferMS)),
414       mTimeouts(*this),
415       mTimeoutIdCounter(1),
416       mNextFiringId(InvalidFiringId + 1),
417 #ifdef DEBUG
418       mFiringIndex(0),
419       mLastFiringIndex(-1),
420 #endif
421       mRunningTimeout(nullptr),
422       mIdleTimeouts(*this),
423       mIdleCallbackTimeoutCounter(1),
424       mLastBudgetUpdate(TimeStamp::Now()),
425       mExecutionBudget(GetMaxBudget(mWindow.IsBackgroundInternal())),
426       mThrottleTimeouts(false),
427       mThrottleTrackingTimeouts(false),
428       mBudgetThrottleTimeouts(false),
429       mIsLoading(false) {
430   MOZ_LOG(gTimeoutLog, LogLevel::Debug,
431           ("TimeoutManager %p created, tracking bucketing %s\n", this,
432            StaticPrefs::privacy_trackingprotection_annotate_channels()
433                ? "enabled"
434                : "disabled"));
435 }
436 
~TimeoutManager()437 TimeoutManager::~TimeoutManager() {
438   MOZ_DIAGNOSTIC_ASSERT(mWindow.IsDying());
439   MOZ_DIAGNOSTIC_ASSERT(!mThrottleTimeoutsTimer);
440 
441   mExecutor->Shutdown();
442   mIdleExecutor->Shutdown();
443 
444   MOZ_LOG(gTimeoutLog, LogLevel::Debug,
445           ("TimeoutManager %p destroyed\n", this));
446 }
447 
GetTimeoutId(Timeout::Reason aReason)448 uint32_t TimeoutManager::GetTimeoutId(Timeout::Reason aReason) {
449   switch (aReason) {
450     case Timeout::Reason::eIdleCallbackTimeout:
451       return ++mIdleCallbackTimeoutCounter;
452     case Timeout::Reason::eTimeoutOrInterval:
453     default:
454       return ++mTimeoutIdCounter;
455   }
456 }
457 
IsRunningTimeout() const458 bool TimeoutManager::IsRunningTimeout() const { return mRunningTimeout; }
459 
SetTimeout(TimeoutHandler * aHandler,int32_t interval,bool aIsInterval,Timeout::Reason aReason,int32_t * aReturn)460 nsresult TimeoutManager::SetTimeout(TimeoutHandler* aHandler, int32_t interval,
461                                     bool aIsInterval, Timeout::Reason aReason,
462                                     int32_t* aReturn) {
463   // If we don't have a document (we could have been unloaded since
464   // the call to setTimeout was made), do nothing.
465   nsCOMPtr<Document> doc = mWindow.GetExtantDoc();
466   if (!doc) {
467     return NS_OK;
468   }
469 
470   // Disallow negative intervals.
471   interval = std::max(0, interval);
472 
473   // Make sure we don't proceed with an interval larger than our timer
474   // code can handle. (Note: we already forced |interval| to be non-negative,
475   // so the uint32_t cast (to avoid compiler warnings) is ok.)
476   uint32_t maxTimeoutMs = PR_IntervalToMilliseconds(DOM_MAX_TIMEOUT_VALUE);
477   if (static_cast<uint32_t>(interval) > maxTimeoutMs) {
478     interval = maxTimeoutMs;
479   }
480 
481   RefPtr<Timeout> timeout = new Timeout();
482 #ifdef DEBUG
483   timeout->mFiringIndex = -1;
484 #endif
485   timeout->mWindow = &mWindow;
486   timeout->mIsInterval = aIsInterval;
487   timeout->mInterval = TimeDuration::FromMilliseconds(interval);
488   timeout->mScriptHandler = aHandler;
489   timeout->mReason = aReason;
490 
491   // No popups from timeouts by default
492   timeout->mPopupState = PopupBlocker::openAbused;
493 
494   timeout->mNestingLevel = sNestingLevel < DOM_CLAMP_TIMEOUT_NESTING_LEVEL
495                                ? sNestingLevel + 1
496                                : sNestingLevel;
497 
498   // Now clamp the actual interval we will use for the timer based on
499   TimeDuration realInterval = CalculateDelay(timeout);
500   TimeStamp now = TimeStamp::Now();
501   timeout->SetWhenOrTimeRemaining(now, realInterval);
502 
503   // If we're not suspended, then set the timer.
504   if (!mWindow.IsSuspended()) {
505     nsresult rv = MaybeSchedule(timeout->When(), now);
506     if (NS_FAILED(rv)) {
507       return rv;
508     }
509   }
510 
511   if (gRunningTimeoutDepth == 0 &&
512       PopupBlocker::GetPopupControlState() < PopupBlocker::openBlocked) {
513     // This timeout is *not* set from another timeout and it's set
514     // while popups are enabled. Propagate the state to the timeout if
515     // its delay (interval) is equal to or less than what
516     // "dom.disable_open_click_delay" is set to (in ms).
517 
518     // This is checking |interval|, not realInterval, on purpose,
519     // because our lower bound for |realInterval| could be pretty high
520     // in some cases.
521     if (interval <= StaticPrefs::dom_disable_open_click_delay()) {
522       timeout->mPopupState = PopupBlocker::GetPopupControlState();
523     }
524   }
525 
526   Timeouts::SortBy sort(mWindow.IsFrozen() ? Timeouts::SortBy::TimeRemaining
527                                            : Timeouts::SortBy::TimeWhen);
528 
529   timeout->mTimeoutId = GetTimeoutId(aReason);
530   mTimeouts.Insert(timeout, sort);
531 
532   *aReturn = timeout->mTimeoutId;
533 
534   MOZ_LOG(
535       gTimeoutLog, LogLevel::Debug,
536       ("Set%s(TimeoutManager=%p, timeout=%p, delay=%i, "
537        "minimum=%f, throttling=%s, state=%s(%s), realInterval=%f) "
538        "returned timeout ID %u, budget=%d\n",
539        aIsInterval ? "Interval" : "Timeout", this, timeout.get(), interval,
540        (CalculateDelay(timeout) - timeout->mInterval).ToMilliseconds(),
541        mThrottleTimeouts ? "yes" : (mThrottleTimeoutsTimer ? "pending" : "no"),
542        IsActive() ? "active" : "inactive",
543        mWindow.IsBackgroundInternal() ? "background" : "foreground",
544        realInterval.ToMilliseconds(), timeout->mTimeoutId,
545        int(mExecutionBudget.ToMilliseconds())));
546 
547   return NS_OK;
548 }
549 
550 // Make sure we clear it no matter which list it's in
ClearTimeout(int32_t aTimerId,Timeout::Reason aReason)551 void TimeoutManager::ClearTimeout(int32_t aTimerId, Timeout::Reason aReason) {
552   if (ClearTimeoutInternal(aTimerId, aReason, false) ||
553       mIdleTimeouts.IsEmpty()) {
554     return;  // no need to check the other list if we cleared the timeout
555   }
556   ClearTimeoutInternal(aTimerId, aReason, true);
557 }
558 
ClearTimeoutInternal(int32_t aTimerId,Timeout::Reason aReason,bool aIsIdle)559 bool TimeoutManager::ClearTimeoutInternal(int32_t aTimerId,
560                                           Timeout::Reason aReason,
561                                           bool aIsIdle) {
562   uint32_t timerId = (uint32_t)aTimerId;
563   Timeouts& timeouts = aIsIdle ? mIdleTimeouts : mTimeouts;
564   RefPtr<TimeoutExecutor>& executor = aIsIdle ? mIdleExecutor : mExecutor;
565   bool deferredDeletion = false;
566 
567   Timeout* timeout = timeouts.GetTimeout(timerId, aReason);
568   if (!timeout) {
569     return false;
570   }
571   bool firstTimeout = timeout == timeouts.GetFirst();
572 
573   MOZ_LOG(gTimeoutLog, LogLevel::Debug,
574           ("%s(TimeoutManager=%p, timeout=%p, ID=%u)\n",
575            timeout->mReason == Timeout::Reason::eIdleCallbackTimeout
576                ? "CancelIdleCallback"
577            : timeout->mIsInterval ? "ClearInterval"
578                                   : "ClearTimeout",
579            this, timeout, timeout->mTimeoutId));
580 
581   if (timeout->mRunning) {
582     /* We're running from inside the timeout. Mark this
583        timeout for deferred deletion by the code in
584        RunTimeout() */
585     timeout->mIsInterval = false;
586     deferredDeletion = true;
587   } else {
588     /* Delete the aTimeout from the pending aTimeout list */
589     timeout->remove();
590   }
591 
592   // We don't need to reschedule the executor if any of the following are true:
593   //  * If the we weren't cancelling the first timeout, then the executor's
594   //    state doesn't need to change.  It will only reflect the next soonest
595   //    Timeout.
596   //  * If we did cancel the first Timeout, but its currently running, then
597   //    RunTimeout() will handle rescheduling the executor.
598   //  * If the window has become suspended then we should not start executing
599   //    Timeouts.
600   if (!firstTimeout || deferredDeletion || mWindow.IsSuspended()) {
601     return true;
602   }
603 
604   // Stop the executor and restart it at the next soonest deadline.
605   executor->Cancel();
606 
607   Timeout* nextTimeout = timeouts.GetFirst();
608   if (nextTimeout) {
609     if (aIsIdle) {
610       MOZ_ALWAYS_SUCCEEDS(
611           executor->MaybeSchedule(nextTimeout->When(), TimeDuration(0)));
612     } else {
613       MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextTimeout->When()));
614     }
615   }
616   return true;
617 }
618 
RunTimeout(const TimeStamp & aNow,const TimeStamp & aTargetDeadline,bool aProcessIdle)619 void TimeoutManager::RunTimeout(const TimeStamp& aNow,
620                                 const TimeStamp& aTargetDeadline,
621                                 bool aProcessIdle) {
622   MOZ_DIAGNOSTIC_ASSERT(!aNow.IsNull());
623   MOZ_DIAGNOSTIC_ASSERT(!aTargetDeadline.IsNull());
624 
625   MOZ_ASSERT_IF(mWindow.IsFrozen(), mWindow.IsSuspended());
626   if (mWindow.IsSuspended()) {
627     return;
628   }
629 
630   Timeouts& timeouts(aProcessIdle ? mIdleTimeouts : mTimeouts);
631 
632   // Limit the overall time spent in RunTimeout() to reduce jank.
633   uint32_t totalTimeLimitMS =
634       std::max(1u, StaticPrefs::dom_timeout_max_consecutive_callbacks_ms());
635   const TimeDuration totalTimeLimit =
636       TimeDuration::Min(TimeDuration::FromMilliseconds(totalTimeLimitMS),
637                         TimeDuration::Max(TimeDuration(), mExecutionBudget));
638 
639   // Allow up to 25% of our total time budget to be used figuring out which
640   // timers need to run.  This is the initial loop in this method.
641   const TimeDuration initialTimeLimit =
642       TimeDuration::FromMilliseconds(totalTimeLimit.ToMilliseconds() / 4);
643 
644   // Ammortize overhead from from calling TimeStamp::Now() in the initial
645   // loop, though, by only checking for an elapsed limit every N timeouts.
646   const uint32_t kNumTimersPerInitialElapsedCheck = 100;
647 
648   // Start measuring elapsed time immediately.  We won't potentially expire
649   // the time budget until at least one Timeout has run, though.
650   TimeStamp now(aNow);
651   TimeStamp start = now;
652 
653   uint32_t firingId = CreateFiringId();
654   auto guard = MakeScopeExit([&] { DestroyFiringId(firingId); });
655 
656   // Make sure that the window and the script context don't go away as
657   // a result of running timeouts
658   RefPtr<nsGlobalWindowInner> window(&mWindow);
659   // Accessing members of mWindow here is safe, because the lifetime of
660   // TimeoutManager is the same as the lifetime of the containing
661   // nsGlobalWindow.
662 
663   // A native timer has gone off. See which of our timeouts need
664   // servicing
665   TimeStamp deadline;
666 
667   if (aTargetDeadline > now) {
668     // The OS timer fired early (which can happen due to the timers
669     // having lower precision than TimeStamp does).  Set |deadline| to
670     // be the time when the OS timer *should* have fired so that any
671     // timers that *should* have fired *will* be fired now.
672 
673     deadline = aTargetDeadline;
674   } else {
675     deadline = now;
676   }
677 
678   TimeStamp nextDeadline;
679   uint32_t numTimersToRun = 0;
680 
681   // The timeout list is kept in deadline order. Discover the latest timeout
682   // whose deadline has expired. On some platforms, native timeout events fire
683   // "early", but we handled that above by setting deadline to aTargetDeadline
684   // if the timer fired early.  So we can stop walking if we get to timeouts
685   // whose When() is greater than deadline, since once that happens we know
686   // nothing past that point is expired.
687 
688   for (Timeout* timeout = timeouts.GetFirst(); timeout != nullptr;
689        timeout = timeout->getNext()) {
690     if (totalTimeLimit.IsZero() || timeout->When() > deadline) {
691       nextDeadline = timeout->When();
692       break;
693     }
694 
695     if (IsInvalidFiringId(timeout->mFiringId)) {
696       // Mark any timeouts that are on the list to be fired with the
697       // firing depth so that we can reentrantly run timeouts
698       timeout->mFiringId = firingId;
699 
700       numTimersToRun += 1;
701 
702       // Run only a limited number of timers based on the configured maximum.
703       if (numTimersToRun % kNumTimersPerInitialElapsedCheck == 0) {
704         now = TimeStamp::Now();
705         TimeDuration elapsed(now - start);
706         if (elapsed >= initialTimeLimit) {
707           nextDeadline = timeout->When();
708           break;
709         }
710       }
711     }
712   }
713   if (aProcessIdle) {
714     MOZ_LOG(
715         gTimeoutLog, LogLevel::Debug,
716         ("Running %u deferred timeouts on idle (TimeoutManager=%p), "
717          "nextDeadline = %gms from now",
718          numTimersToRun, this,
719          nextDeadline.IsNull() ? 0.0 : (nextDeadline - now).ToMilliseconds()));
720   }
721 
722   now = TimeStamp::Now();
723 
724   // Wherever we stopped in the timer list, schedule the executor to
725   // run for the next unexpired deadline.  Note, this *must* be done
726   // before we start executing any content script handlers.  If one
727   // of them spins the event loop the executor must already be scheduled
728   // in order for timeouts to fire properly.
729   if (!nextDeadline.IsNull()) {
730     // Note, we verified the window is not suspended at the top of
731     // method and the window should not have been suspended while
732     // executing the loop above since it doesn't call out to js.
733     MOZ_DIAGNOSTIC_ASSERT(!mWindow.IsSuspended());
734     if (aProcessIdle) {
735       // We don't want to update timing budget for idle queue firings, and
736       // all timeouts in the IdleTimeouts list have hit their deadlines,
737       // and so should run as soon as possible.
738       MOZ_ALWAYS_SUCCEEDS(
739           mIdleExecutor->MaybeSchedule(nextDeadline, TimeDuration()));
740     } else {
741       MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextDeadline, now));
742     }
743   }
744 
745   // Maybe the timeout that the event was fired for has been deleted
746   // and there are no others timeouts with deadlines that make them
747   // eligible for execution yet. Go away.
748   if (!numTimersToRun) {
749     return;
750   }
751 
752   // Now we need to search the normal and tracking timer list at the same
753   // time to run the timers in the scheduled order.
754 
755   // We stop iterating each list when we go past the last expired timeout from
756   // that list that we have observed above.  That timeout will either be the
757   // next item after the last timeout we looked at or nullptr if we have
758   // exhausted the entire list while looking for the last expired timeout.
759   {
760     // Use a nested scope in order to make sure the strong references held while
761     // iterating are freed after the loop.
762 
763     // The next timeout to run. This is used to advance the loop, but
764     // we cannot set it until we've run the current timeout, since
765     // running the current timeout might remove the immediate next
766     // timeout.
767     RefPtr<Timeout> next;
768 
769     for (RefPtr<Timeout> timeout = timeouts.GetFirst(); timeout != nullptr;
770          timeout = next) {
771       next = timeout->getNext();
772       // We should only execute callbacks for the set of expired Timeout
773       // objects we computed above.
774       if (timeout->mFiringId != firingId) {
775         // If the FiringId does not match, but is still valid, then this is
776         // a Timeout for another RunTimeout() on the call stack (such as in
777         // the case of nested event loops, for alert() or more likely XHR).
778         // Just skip it.
779         if (IsValidFiringId(timeout->mFiringId)) {
780           MOZ_LOG(gTimeoutLog, LogLevel::Debug,
781                   ("Skipping Run%s(TimeoutManager=%p, timeout=%p) since "
782                    "firingId %d is valid (processing firingId %d)"
783 #ifdef DEBUG
784                    " - FiringIndex %" PRId64 " (mLastFiringIndex %" PRId64 ")"
785 #endif
786                    ,
787                    timeout->mIsInterval ? "Interval" : "Timeout", this,
788                    timeout.get(), timeout->mFiringId, firingId
789 #ifdef DEBUG
790                    ,
791                    timeout->mFiringIndex, mFiringIndex
792 #endif
793                    ));
794 #ifdef DEBUG
795           // The old FiringIndex assumed no recursion; recursion can cause
796           // other timers to get fired "in the middle" of a sequence we've
797           // already assigned firingindexes to.  Since we're not going to
798           // run this timeout now, remove any FiringIndex that was already
799           // set.
800 
801           // Since all timers that have FiringIndexes set *must* be ready
802           // to run and have valid FiringIds, all of them will be 'skipped'
803           // and reset if we recurse - we don't have to look through the
804           // list past where we'll stop on the first InvalidFiringId.
805           timeout->mFiringIndex = -1;
806 #endif
807           continue;
808         }
809 
810         // If, however, the FiringId is invalid then we have reached Timeout
811         // objects beyond the list we calculated above.  This can happen
812         // if the Timeout just beyond our last expired Timeout is cancelled
813         // by one of the callbacks we've just executed.  In this case we
814         // should just stop iterating.  We're done.
815         else {
816           break;
817         }
818       }
819 
820       MOZ_ASSERT_IF(mWindow.IsFrozen(), mWindow.IsSuspended());
821       if (mWindow.IsSuspended()) {
822         break;
823       }
824 
825       // The timeout is on the list to run at this depth, go ahead and
826       // process it.
827 
828       // Record the first time we try to fire a timeout, and ensure that
829       // all actual firings occur in that order.  This ensures that we
830       // retain compliance with the spec language
831       // (https://html.spec.whatwg.org/#dom-settimeout) specifically items
832       // 15 ("If method context is a Window object, wait until the Document
833       // associated with method context has been fully active for a further
834       // timeout milliseconds (not necessarily consecutively)") and item 16
835       // ("Wait until any invocations of this algorithm that had the same
836       // method context, that started before this one, and whose timeout is
837       // equal to or less than this one's, have completed.").
838 #ifdef DEBUG
839       if (timeout->mFiringIndex == -1) {
840         timeout->mFiringIndex = mFiringIndex++;
841       }
842 #endif
843 
844       if (mIsLoading && !aProcessIdle) {
845         // Any timeouts that would fire during a load will be deferred
846         // until the load event occurs, but if there's an idle time,
847         // they'll be run before the load event.
848         timeout->remove();
849         // MOZ_RELEASE_ASSERT(timeout->When() <= (TimeStamp::Now()));
850         mIdleTimeouts.InsertBack(timeout);
851         if (MOZ_LOG_TEST(gTimeoutLog, LogLevel::Debug)) {
852           uint32_t num = 0;
853           for (Timeout* t = mIdleTimeouts.GetFirst(); t != nullptr;
854                t = t->getNext()) {
855             num++;
856           }
857           MOZ_LOG(
858               gTimeoutLog, LogLevel::Debug,
859               ("Deferring Run%s(TimeoutManager=%p, timeout=%p (%gms in the "
860                "past)) (%u deferred)",
861                timeout->mIsInterval ? "Interval" : "Timeout", this,
862                timeout.get(), (now - timeout->When()).ToMilliseconds(), num));
863         }
864         MOZ_ALWAYS_SUCCEEDS(mIdleExecutor->MaybeSchedule(now, TimeDuration()));
865       } else {
866         // Get the script context (a strong ref to prevent it going away)
867         // for this timeout and ensure the script language is enabled.
868         nsCOMPtr<nsIScriptContext> scx = mWindow.GetContextInternal();
869 
870         if (!scx) {
871           // No context means this window was closed or never properly
872           // initialized for this language.  This timer will never fire
873           // so just remove it.
874           timeout->remove();
875           continue;
876         }
877 
878 #ifdef DEBUG
879         if (timeout->mFiringIndex <= mLastFiringIndex) {
880           MOZ_LOG(gTimeoutLog, LogLevel::Debug,
881                   ("Incorrect firing index for Run%s(TimeoutManager=%p, "
882                    "timeout=%p) with "
883                    "firingId %d - FiringIndex %" PRId64
884                    " (mLastFiringIndex %" PRId64 ")",
885                    timeout->mIsInterval ? "Interval" : "Timeout", this,
886                    timeout.get(), timeout->mFiringId, timeout->mFiringIndex,
887                    mFiringIndex));
888         }
889         MOZ_ASSERT(timeout->mFiringIndex > mLastFiringIndex);
890         mLastFiringIndex = timeout->mFiringIndex;
891 #endif
892         // This timeout is good to run.
893         bool timeout_was_cleared = window->RunTimeoutHandler(timeout, scx);
894         MOZ_LOG(gTimeoutLog, LogLevel::Debug,
895                 ("Run%s(TimeoutManager=%p, timeout=%p) returned %d\n",
896                  timeout->mIsInterval ? "Interval" : "Timeout", this,
897                  timeout.get(), !!timeout_was_cleared));
898 
899         if (timeout_was_cleared) {
900           // Make sure we're not holding any Timeout objects alive.
901           next = nullptr;
902 
903           // Since ClearAllTimeouts() was called the lists should be empty.
904           MOZ_DIAGNOSTIC_ASSERT(!HasTimeouts());
905 
906           return;
907         }
908 
909         // If we need to reschedule a setInterval() the delay should be
910         // calculated based on when its callback started to execute.  So
911         // save off the last time before updating our "now" timestamp to
912         // account for its callback execution time.
913         TimeStamp lastCallbackTime = now;
914         now = TimeStamp::Now();
915 
916         // If we have a regular interval timer, we re-schedule the
917         // timeout, accounting for clock drift.
918         bool needsReinsertion =
919             RescheduleTimeout(timeout, lastCallbackTime, now);
920 
921         // Running a timeout can cause another timeout to be deleted, so
922         // we need to reset the pointer to the following timeout.
923         next = timeout->getNext();
924 
925         timeout->remove();
926 
927         if (needsReinsertion) {
928           // Insert interval timeout onto the corresponding list sorted in
929           // deadline order. AddRefs timeout.
930           // Always re-insert into the normal time queue!
931           mTimeouts.Insert(timeout, mWindow.IsFrozen()
932                                         ? Timeouts::SortBy::TimeRemaining
933                                         : Timeouts::SortBy::TimeWhen);
934         }
935       }
936       // Check to see if we have run out of time to execute timeout handlers.
937       // If we've exceeded our time budget then terminate the loop immediately.
938       TimeDuration elapsed = now - start;
939       if (elapsed >= totalTimeLimit) {
940         // We ran out of time.  Make sure to schedule the executor to
941         // run immediately for the next timer, if it exists.  Its possible,
942         // however, that the last timeout handler suspended the window.  If
943         // that happened then we must skip this step.
944         if (!mWindow.IsSuspended()) {
945           if (next) {
946             if (aProcessIdle) {
947               // We don't want to update timing budget for idle queue firings,
948               // and all timeouts in the IdleTimeouts list have hit their
949               // deadlines, and so should run as soon as possible.
950 
951               // Shouldn't need cancelling since it never waits
952               MOZ_ALWAYS_SUCCEEDS(
953                   mIdleExecutor->MaybeSchedule(next->When(), TimeDuration()));
954             } else {
955               // If we ran out of execution budget we need to force a
956               // reschedule. By cancelling the executor we will not run
957               // immediately, but instead reschedule to the minimum
958               // scheduling delay.
959               if (mExecutionBudget < TimeDuration()) {
960                 mExecutor->Cancel();
961               }
962 
963               MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(next->When(), now));
964             }
965           }
966         }
967         break;
968       }
969     }
970   }
971 }
972 
RescheduleTimeout(Timeout * aTimeout,const TimeStamp & aLastCallbackTime,const TimeStamp & aCurrentNow)973 bool TimeoutManager::RescheduleTimeout(Timeout* aTimeout,
974                                        const TimeStamp& aLastCallbackTime,
975                                        const TimeStamp& aCurrentNow) {
976   MOZ_DIAGNOSTIC_ASSERT(aLastCallbackTime <= aCurrentNow);
977 
978   if (!aTimeout->mIsInterval) {
979     return false;
980   }
981 
982   // Automatically increase the nesting level when a setInterval()
983   // is rescheduled just as if it was using a chained setTimeout().
984   if (aTimeout->mNestingLevel < DOM_CLAMP_TIMEOUT_NESTING_LEVEL) {
985     aTimeout->mNestingLevel += 1;
986   }
987 
988   // Compute time to next timeout for interval timer.
989   // Make sure nextInterval is at least CalculateDelay().
990   TimeDuration nextInterval = CalculateDelay(aTimeout);
991 
992   TimeStamp firingTime = aLastCallbackTime + nextInterval;
993   TimeDuration delay = firingTime - aCurrentNow;
994 
995 #ifdef DEBUG
996   aTimeout->mFiringIndex = -1;
997 #endif
998   // And make sure delay is nonnegative; that might happen if the timer
999   // thread is firing our timers somewhat early or if they're taking a long
1000   // time to run the callback.
1001   if (delay < TimeDuration(0)) {
1002     delay = TimeDuration(0);
1003   }
1004 
1005   aTimeout->SetWhenOrTimeRemaining(aCurrentNow, delay);
1006 
1007   if (mWindow.IsSuspended()) {
1008     return true;
1009   }
1010 
1011   nsresult rv = MaybeSchedule(aTimeout->When(), aCurrentNow);
1012   NS_ENSURE_SUCCESS(rv, false);
1013 
1014   return true;
1015 }
1016 
ClearAllTimeouts()1017 void TimeoutManager::ClearAllTimeouts() {
1018   bool seenRunningTimeout = false;
1019 
1020   MOZ_LOG(gTimeoutLog, LogLevel::Debug,
1021           ("ClearAllTimeouts(TimeoutManager=%p)\n", this));
1022 
1023   if (mThrottleTimeoutsTimer) {
1024     mThrottleTimeoutsTimer->Cancel();
1025     mThrottleTimeoutsTimer = nullptr;
1026   }
1027 
1028   mExecutor->Cancel();
1029   mIdleExecutor->Cancel();
1030 
1031   ForEachUnorderedTimeout([&](Timeout* aTimeout) {
1032     /* If RunTimeout() is higher up on the stack for this
1033        window, e.g. as a result of document.write from a timeout,
1034        then we need to reset the list insertion point for
1035        newly-created timeouts in case the user adds a timeout,
1036        before we pop the stack back to RunTimeout. */
1037     if (mRunningTimeout == aTimeout) {
1038       seenRunningTimeout = true;
1039     }
1040 
1041     // Set timeout->mCleared to true to indicate that the timeout was
1042     // cleared and taken out of the list of timeouts
1043     aTimeout->mCleared = true;
1044   });
1045 
1046   // Clear out our lists
1047   mTimeouts.Clear();
1048   mIdleTimeouts.Clear();
1049 }
1050 
Insert(Timeout * aTimeout,SortBy aSortBy)1051 void TimeoutManager::Timeouts::Insert(Timeout* aTimeout, SortBy aSortBy) {
1052   // Start at mLastTimeout and go backwards.  Stop if we see a Timeout with a
1053   // valid FiringId since those timers are currently being processed by
1054   // RunTimeout.  This optimizes for the common case of insertion at the end.
1055   Timeout* prevSibling;
1056   for (prevSibling = GetLast();
1057        prevSibling &&
1058        // This condition needs to match the one in SetTimeoutOrInterval that
1059        // determines whether to set When() or TimeRemaining().
1060        (aSortBy == SortBy::TimeRemaining
1061             ? prevSibling->TimeRemaining() > aTimeout->TimeRemaining()
1062             : prevSibling->When() > aTimeout->When()) &&
1063        // Check the firing ID last since it will evaluate true in the vast
1064        // majority of cases.
1065        mManager.IsInvalidFiringId(prevSibling->mFiringId);
1066        prevSibling = prevSibling->getPrevious()) {
1067     /* Do nothing; just searching */
1068   }
1069 
1070   // Now link in aTimeout after prevSibling.
1071   if (prevSibling) {
1072     aTimeout->SetTimeoutContainer(mTimeouts);
1073     prevSibling->setNext(aTimeout);
1074   } else {
1075     InsertFront(aTimeout);
1076   }
1077 
1078   aTimeout->mFiringId = InvalidFiringId;
1079 }
1080 
BeginRunningTimeout(Timeout * aTimeout)1081 Timeout* TimeoutManager::BeginRunningTimeout(Timeout* aTimeout) {
1082   Timeout* currentTimeout = mRunningTimeout;
1083   mRunningTimeout = aTimeout;
1084   ++gRunningTimeoutDepth;
1085 
1086   RecordExecution(currentTimeout, aTimeout);
1087   return currentTimeout;
1088 }
1089 
EndRunningTimeout(Timeout * aTimeout)1090 void TimeoutManager::EndRunningTimeout(Timeout* aTimeout) {
1091   --gRunningTimeoutDepth;
1092 
1093   RecordExecution(mRunningTimeout, aTimeout);
1094   mRunningTimeout = aTimeout;
1095 }
1096 
UnmarkGrayTimers()1097 void TimeoutManager::UnmarkGrayTimers() {
1098   ForEachUnorderedTimeout([](Timeout* aTimeout) {
1099     if (aTimeout->mScriptHandler) {
1100       aTimeout->mScriptHandler->MarkForCC();
1101     }
1102   });
1103 }
1104 
Suspend()1105 void TimeoutManager::Suspend() {
1106   MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Suspend(TimeoutManager=%p)\n", this));
1107 
1108   if (mThrottleTimeoutsTimer) {
1109     mThrottleTimeoutsTimer->Cancel();
1110     mThrottleTimeoutsTimer = nullptr;
1111   }
1112 
1113   mExecutor->Cancel();
1114   mIdleExecutor->Cancel();
1115 }
1116 
Resume()1117 void TimeoutManager::Resume() {
1118   MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Resume(TimeoutManager=%p)\n", this));
1119 
1120   // When Suspend() has been called after IsDocumentLoaded(), but the
1121   // throttle tracking timer never managed to fire, start the timer
1122   // again.
1123   if (mWindow.IsDocumentLoaded() && !mThrottleTimeouts) {
1124     MaybeStartThrottleTimeout();
1125   }
1126 
1127   Timeout* nextTimeout = mTimeouts.GetFirst();
1128   if (nextTimeout) {
1129     MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextTimeout->When()));
1130   }
1131   nextTimeout = mIdleTimeouts.GetFirst();
1132   if (nextTimeout) {
1133     MOZ_ALWAYS_SUCCEEDS(
1134         mIdleExecutor->MaybeSchedule(nextTimeout->When(), TimeDuration()));
1135   }
1136 }
1137 
Freeze()1138 void TimeoutManager::Freeze() {
1139   MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Freeze(TimeoutManager=%p)\n", this));
1140 
1141   TimeStamp now = TimeStamp::Now();
1142   ForEachUnorderedTimeout([&](Timeout* aTimeout) {
1143     // Save the current remaining time for this timeout.  We will
1144     // re-apply it when the window is Thaw()'d.  This effectively
1145     // shifts timers to the right as if time does not pass while
1146     // the window is frozen.
1147     TimeDuration delta(0);
1148     if (aTimeout->When() > now) {
1149       delta = aTimeout->When() - now;
1150     }
1151     aTimeout->SetWhenOrTimeRemaining(now, delta);
1152     MOZ_DIAGNOSTIC_ASSERT(aTimeout->TimeRemaining() == delta);
1153   });
1154 }
1155 
Thaw()1156 void TimeoutManager::Thaw() {
1157   MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Thaw(TimeoutManager=%p)\n", this));
1158 
1159   TimeStamp now = TimeStamp::Now();
1160 
1161   ForEachUnorderedTimeout([&](Timeout* aTimeout) {
1162     // Set When() back to the time when the timer is supposed to fire.
1163     aTimeout->SetWhenOrTimeRemaining(now, aTimeout->TimeRemaining());
1164     MOZ_DIAGNOSTIC_ASSERT(!aTimeout->When().IsNull());
1165   });
1166 }
1167 
UpdateBackgroundState()1168 void TimeoutManager::UpdateBackgroundState() {
1169   mExecutionBudget = GetMaxBudget(mWindow.IsBackgroundInternal());
1170 
1171   // When the window moves to the background or foreground we should
1172   // reschedule the TimeoutExecutor in case the MinSchedulingDelay()
1173   // changed.  Only do this if the window is not suspended and we
1174   // actually have a timeout.
1175   if (!mWindow.IsSuspended()) {
1176     Timeout* nextTimeout = mTimeouts.GetFirst();
1177     if (nextTimeout) {
1178       mExecutor->Cancel();
1179       MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextTimeout->When()));
1180     }
1181     // the Idle queue should all be past their firing time, so there we just
1182     // need to restart the queue
1183 
1184     // XXX May not be needed if we don't stop the idle queue, as
1185     // MinSchedulingDelay isn't relevant here
1186     nextTimeout = mIdleTimeouts.GetFirst();
1187     if (nextTimeout) {
1188       mIdleExecutor->Cancel();
1189       MOZ_ALWAYS_SUCCEEDS(
1190           mIdleExecutor->MaybeSchedule(nextTimeout->When(), TimeDuration()));
1191     }
1192   }
1193 }
1194 
1195 namespace {
1196 
1197 class ThrottleTimeoutsCallback final : public nsITimerCallback,
1198                                        public nsINamed {
1199  public:
ThrottleTimeoutsCallback(nsGlobalWindowInner * aWindow)1200   explicit ThrottleTimeoutsCallback(nsGlobalWindowInner* aWindow)
1201       : mWindow(aWindow) {}
1202 
1203   NS_DECL_ISUPPORTS
1204   NS_DECL_NSITIMERCALLBACK
1205 
GetName(nsACString & aName)1206   NS_IMETHOD GetName(nsACString& aName) override {
1207     aName.AssignLiteral("ThrottleTimeoutsCallback");
1208     return NS_OK;
1209   }
1210 
1211  private:
1212   ~ThrottleTimeoutsCallback() = default;
1213 
1214  private:
1215   // The strong reference here keeps the Window and hence the TimeoutManager
1216   // object itself alive.
1217   RefPtr<nsGlobalWindowInner> mWindow;
1218 };
1219 
NS_IMPL_ISUPPORTS(ThrottleTimeoutsCallback,nsITimerCallback,nsINamed)1220 NS_IMPL_ISUPPORTS(ThrottleTimeoutsCallback, nsITimerCallback, nsINamed)
1221 
1222 NS_IMETHODIMP
1223 ThrottleTimeoutsCallback::Notify(nsITimer* aTimer) {
1224   mWindow->TimeoutManager().StartThrottlingTimeouts();
1225   mWindow = nullptr;
1226   return NS_OK;
1227 }
1228 
1229 }  // namespace
1230 
BudgetThrottlingEnabled(bool aIsBackground) const1231 bool TimeoutManager::BudgetThrottlingEnabled(bool aIsBackground) const {
1232   // A window can be throttled using budget if
1233   // * It isn't active
1234   // * If it isn't using WebRTC
1235   // * If it hasn't got open WebSockets
1236   // * If it hasn't got active IndexedDB databases
1237 
1238   // Note that we allow both foreground and background to be
1239   // considered for budget throttling. What determines if they are if
1240   // budget throttling is enabled is the max budget.
1241   if ((aIsBackground
1242            ? StaticPrefs::dom_timeout_background_throttling_max_budget()
1243            : StaticPrefs::dom_timeout_foreground_throttling_max_budget()) < 0) {
1244     return false;
1245   }
1246 
1247   if (!mBudgetThrottleTimeouts || IsActive()) {
1248     return false;
1249   }
1250 
1251   // Check if there are any active IndexedDB databases
1252   if (mWindow.HasActiveIndexedDBDatabases()) {
1253     return false;
1254   }
1255 
1256   // Check if we have active PeerConnection
1257   if (mWindow.HasActivePeerConnections()) {
1258     return false;
1259   }
1260 
1261   if (mWindow.HasOpenWebSockets()) {
1262     return false;
1263   }
1264 
1265   return true;
1266 }
1267 
StartThrottlingTimeouts()1268 void TimeoutManager::StartThrottlingTimeouts() {
1269   MOZ_ASSERT(NS_IsMainThread());
1270   MOZ_DIAGNOSTIC_ASSERT(mThrottleTimeoutsTimer);
1271 
1272   MOZ_LOG(gTimeoutLog, LogLevel::Debug,
1273           ("TimeoutManager %p started to throttle tracking timeouts\n", this));
1274 
1275   MOZ_DIAGNOSTIC_ASSERT(!mThrottleTimeouts);
1276   mThrottleTimeouts = true;
1277   mThrottleTrackingTimeouts = true;
1278   mBudgetThrottleTimeouts =
1279       StaticPrefs::dom_timeout_enable_budget_timer_throttling();
1280   mThrottleTimeoutsTimer = nullptr;
1281 }
1282 
OnDocumentLoaded()1283 void TimeoutManager::OnDocumentLoaded() {
1284   // The load event may be firing again if we're coming back to the page by
1285   // navigating through the session history, so we need to ensure to only call
1286   // this when mThrottleTimeouts hasn't been set yet.
1287   if (!mThrottleTimeouts) {
1288     MaybeStartThrottleTimeout();
1289   }
1290 }
1291 
MaybeStartThrottleTimeout()1292 void TimeoutManager::MaybeStartThrottleTimeout() {
1293   if (StaticPrefs::dom_timeout_throttling_delay() <= 0 || mWindow.IsDying() ||
1294       mWindow.IsSuspended()) {
1295     return;
1296   }
1297 
1298   MOZ_DIAGNOSTIC_ASSERT(!mThrottleTimeouts);
1299 
1300   MOZ_LOG(gTimeoutLog, LogLevel::Debug,
1301           ("TimeoutManager %p delaying tracking timeout throttling by %dms\n",
1302            this, StaticPrefs::dom_timeout_throttling_delay()));
1303 
1304   nsCOMPtr<nsITimerCallback> callback = new ThrottleTimeoutsCallback(&mWindow);
1305 
1306   NS_NewTimerWithCallback(getter_AddRefs(mThrottleTimeoutsTimer), callback,
1307                           StaticPrefs::dom_timeout_throttling_delay(),
1308                           nsITimer::TYPE_ONE_SHOT, EventTarget());
1309 }
1310 
BeginSyncOperation()1311 void TimeoutManager::BeginSyncOperation() {
1312   // If we're beginning a sync operation, the currently running
1313   // timeout will be put on hold. To not get into an inconsistent
1314   // state, where the currently running timeout appears to take time
1315   // equivalent to the period of us spinning up a new event loop,
1316   // record what we have and stop recording until we reach
1317   // EndSyncOperation.
1318   RecordExecution(mRunningTimeout, nullptr);
1319 }
1320 
EndSyncOperation()1321 void TimeoutManager::EndSyncOperation() {
1322   // If we're running a timeout, restart the measurement from here.
1323   RecordExecution(nullptr, mRunningTimeout);
1324 }
1325 
EventTarget()1326 nsIEventTarget* TimeoutManager::EventTarget() {
1327   return mWindow.GetBrowsingContextGroup()->GetTimerEventQueue();
1328 }
1329