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