Backed out changeset 9d8b4c0b99ed (bug 1945683) for causing btime failures. CLOSED...
[gecko.git] / dom / base / TimeoutManager.cpp
blob64c41f411467c8f3ef7509fa211d80a5d76f3811
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 "nsIGlobalObject.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"
27 #include "mozilla/dom/WorkerScope.h"
29 using namespace mozilla;
30 using namespace mozilla::dom;
32 LazyLogModule gTimeoutLog("Timeout");
34 static int32_t gRunningTimeoutDepth = 0;
36 // static
37 const uint32_t TimeoutManager::InvalidFiringId = 0;
39 namespace {
40 double GetRegenerationFactor(bool aIsBackground) {
41 // Lookup function for "dom.timeout.{background,
42 // foreground}_budget_regeneration_rate".
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;
57 TimeDuration GetMaxBudget(bool aIsBackground) {
58 // Lookup function for "dom.timeout.{background,
59 // foreground}_throttling_max_budget".
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();
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));
85 } // namespace
89 bool TimeoutManager::IsBackground() const {
90 return !IsActive() && mGlobalObject.IsBackgroundInternal();
93 bool TimeoutManager::IsActive() const {
94 // A window/worker is considered active if:
95 // * It is a chrome window
96 // * It is playing audio
98 // Note that a window/worker can be considered active if it is either in the
99 // foreground or in the background.
101 nsGlobalWindowInner* window = GetInnerWindow();
102 if (window && window->IsChromeWindow()) {
103 return true;
106 // Check if we're playing audio
107 if (mGlobalObject.IsPlayingAudio()) {
108 return true;
111 return false;
114 void TimeoutManager::SetLoading(bool value) {
115 // When moving from loading to non-loading, we may need to
116 // reschedule any existing timeouts from the idle timeout queue
117 // to the normal queue.
118 MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("%p: SetLoading(%d)", this, value));
119 if (mIsLoading && !value) {
120 MoveIdleToActive();
122 // We don't immediately move existing timeouts to the idle queue if we
123 // move to loading. When they would have fired, we'll see we're loading
124 // and move them then.
125 mIsLoading = value;
128 void TimeoutManager::MoveIdleToActive() {
129 uint32_t num = 0;
130 TimeStamp when;
131 TimeStamp now;
132 // Ensure we maintain the ordering of timeouts, so timeouts
133 // never fire before a timeout set for an earlier time, or
134 // before a timeout for the same time already submitted.
135 // See https://html.spec.whatwg.org/#dom-settimeout #16 and #17
136 while (RefPtr<Timeout> timeout = mIdleTimeouts.GetLast()) {
137 if (num == 0) {
138 when = timeout->When();
140 timeout->remove();
141 mTimeouts.InsertFront(timeout);
142 if (profiler_thread_is_being_profiled_for_markers()) {
143 if (num == 0) {
144 now = TimeStamp::Now();
146 TimeDuration elapsed = now - timeout->SubmitTime();
147 TimeDuration target = timeout->When() - timeout->SubmitTime();
148 TimeDuration delta = now - timeout->When();
149 if (mGlobalObject.GetAsInnerWindow()) {
150 nsPrintfCString marker(
151 "Releasing deferred setTimeout() for %dms (original target time "
152 "was "
153 "%dms (%dms delta))",
154 int(elapsed.ToMilliseconds()), int(target.ToMilliseconds()),
155 int(delta.ToMilliseconds()));
156 // don't have end before start...
157 PROFILER_MARKER_TEXT(
158 "setTimeout deferred release", DOM,
159 MarkerOptions(
160 MarkerTiming::Interval(
161 delta.ToMilliseconds() >= 0 ? timeout->When() : now, now),
162 MarkerInnerWindowId(
163 mGlobalObject.GetAsInnerWindow()->WindowID())),
164 marker);
166 // TODO: add separate marker for Workers case
168 num++;
170 if (num > 0) {
171 MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(when));
172 mIdleExecutor->Cancel();
174 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
175 ("%p: Moved %d timeouts from Idle to active", this, num));
178 uint32_t TimeoutManager::CreateFiringId() {
179 uint32_t id = mNextFiringId;
180 mNextFiringId += 1;
181 if (mNextFiringId == InvalidFiringId) {
182 mNextFiringId += 1;
185 mFiringIdStack.AppendElement(id);
187 return id;
190 void TimeoutManager::DestroyFiringId(uint32_t aFiringId) {
191 MOZ_DIAGNOSTIC_ASSERT(!mFiringIdStack.IsEmpty());
192 MOZ_DIAGNOSTIC_ASSERT(mFiringIdStack.LastElement() == aFiringId);
193 mFiringIdStack.RemoveLastElement();
196 bool TimeoutManager::IsValidFiringId(uint32_t aFiringId) const {
197 return !IsInvalidFiringId(aFiringId);
200 TimeDuration TimeoutManager::MinSchedulingDelay() const {
201 if (IsActive()) {
202 return TimeDuration();
205 // do not throttle workers if dom_workers_throttling is disabled
206 if (!GetInnerWindow() && StaticPrefs::dom_workers_timeoutmanager() &&
207 !StaticPrefs::dom_workers_throttling_enabled()) {
208 return TimeDuration();
211 bool isBackground = mGlobalObject.IsBackgroundInternal();
213 // If a window/worker isn't active as defined by TimeoutManager::IsActive()
214 // and we're throttling timeouts using an execution budget, we
215 // should adjust the minimum scheduling delay if we have used up all
216 // of our execution budget. Note that a window/worker can be active or
217 // inactive regardless of wether it is in the foreground or in the
218 // background. Throttling using a budget depends largely on the
219 // regeneration factor, which can be specified separately for
220 // foreground and background windows.
222 // The value that we compute is the time in the future when we again
223 // have a positive execution budget. We do this by taking the
224 // execution budget into account, which if it positive implies that
225 // we have time left to execute, and if it is negative implies that
226 // we should throttle it until the budget again is positive. The
227 // factor used is the rate of budget regeneration.
229 // We clamp the delay to be less than or equal to
230 // "dom.timeout.budget_throttling_max_delay" to not entirely starve
231 // the timeouts.
233 // Consider these examples assuming we should throttle using
234 // budgets:
236 // mExecutionBudget is 20ms
237 // factor is 1, which is 1 ms/ms
238 // delay is 0ms
239 // then we will compute the minimum delay:
240 // max(0, - 20 * 1) = 0
242 // mExecutionBudget is -50ms
243 // factor is 0.1, which is 1 ms/10ms
244 // delay is 1000ms
245 // then we will compute the minimum delay:
246 // max(1000, - (- 50) * 1/0.1) = max(1000, 500) = 1000
248 // mExecutionBudget is -15ms
249 // factor is 0.01, which is 1 ms/100ms
250 // delay is 1000ms
251 // then we will compute the minimum delay:
252 // max(1000, - (- 15) * 1/0.01) = max(1000, 1500) = 1500
253 TimeDuration unthrottled =
254 isBackground ? TimeDuration::FromMilliseconds(
255 StaticPrefs::dom_min_background_timeout_value())
256 : TimeDuration();
257 bool budgetThrottlingEnabled = BudgetThrottlingEnabled(isBackground);
258 if (budgetThrottlingEnabled && mExecutionBudget < TimeDuration()) {
259 // Only throttle if execution budget is less than 0
261 double factor = 1.0 / GetRegenerationFactor(isBackground);
262 return TimeDuration::Max(unthrottled, -mExecutionBudget.MultDouble(factor));
264 if (!budgetThrottlingEnabled && isBackground) {
265 return TimeDuration::FromMilliseconds(
266 StaticPrefs::
267 dom_min_background_timeout_value_without_budget_throttling());
270 return unthrottled;
273 nsresult TimeoutManager::MaybeSchedule(const TimeStamp& aWhen,
274 const TimeStamp& aNow) {
275 MOZ_DIAGNOSTIC_ASSERT(mExecutor);
277 // Before we can schedule the executor we need to make sure that we
278 // have an updated execution budget.
279 UpdateBudget(aNow);
280 return mExecutor->MaybeSchedule(aWhen, MinSchedulingDelay());
283 bool TimeoutManager::IsInvalidFiringId(uint32_t aFiringId) const {
284 // Check the most common ways to invalidate a firing id first.
285 // These should be quite fast.
286 if (aFiringId == InvalidFiringId || mFiringIdStack.IsEmpty()) {
287 return true;
290 if (mFiringIdStack.Length() == 1) {
291 return mFiringIdStack[0] != aFiringId;
294 // Next do a range check on the first and last items in the stack
295 // of active firing ids. This is a bit slower.
296 uint32_t low = mFiringIdStack[0];
297 uint32_t high = mFiringIdStack.LastElement();
298 MOZ_DIAGNOSTIC_ASSERT(low != high);
299 if (low > high) {
300 // If the first element is bigger than the last element in the
301 // stack, that means mNextFiringId wrapped around to zero at
302 // some point.
303 std::swap(low, high);
305 MOZ_DIAGNOSTIC_ASSERT(low < high);
307 if (aFiringId < low || aFiringId > high) {
308 return true;
311 // Finally, fall back to verifying the firing id is not anywhere
312 // in the stack. This could be slow for a large stack, but that
313 // should be rare. It can only happen with deeply nested event
314 // loop spinning. For example, a page that does a lot of timers
315 // and a lot of sync XHRs within those timers could be slow here.
316 return !mFiringIdStack.Contains(aFiringId);
319 TimeDuration TimeoutManager::CalculateDelay(Timeout* aTimeout) const {
320 MOZ_DIAGNOSTIC_ASSERT(aTimeout);
321 TimeDuration result = aTimeout->mInterval;
323 if (aTimeout->mNestingLevel >=
324 StaticPrefs::dom_clamp_timeout_nesting_level_AtStartup()) {
325 uint32_t minTimeoutValue = StaticPrefs::dom_min_timeout_value();
326 result = TimeDuration::Max(result,
327 TimeDuration::FromMilliseconds(minTimeoutValue));
330 return result;
333 void TimeoutManager::RecordExecution(Timeout* aRunningTimeout,
334 Timeout* aTimeout) {
335 TimeoutBudgetManager& budgetManager = TimeoutBudgetManager::Get();
336 TimeStamp now = TimeStamp::Now();
338 if (aRunningTimeout) {
339 // If we're running a timeout callback, record any execution until
340 // now.
341 TimeDuration duration = budgetManager.RecordExecution(now, aRunningTimeout);
343 UpdateBudget(now, duration);
346 if (aTimeout) {
347 // If we're starting a new timeout callback, start recording.
348 budgetManager.StartRecording(now);
349 } else {
350 // Else stop by clearing the start timestamp.
351 budgetManager.StopRecording();
355 void TimeoutManager::UpdateBudget(const TimeStamp& aNow,
356 const TimeDuration& aDuration) {
357 nsGlobalWindowInner* window = GetInnerWindow();
358 if (!window) {
359 return;
362 if (window->IsChromeWindow()) {
363 return;
366 // The budget is adjusted by increasing it with the time since the
367 // last budget update factored with the regeneration rate. If a
368 // runnable has executed, subtract that duration from the
369 // budget. The budget updated without consideration of wether the
370 // window/worker is active or not. If throttling is enabled and the
371 // window/worker is active and then becomes inactive, an overdrawn budget will
372 // still be counted against the minimum delay.
373 bool isBackground = mGlobalObject.IsBackgroundInternal();
374 if (BudgetThrottlingEnabled(isBackground)) {
375 double factor = GetRegenerationFactor(isBackground);
376 TimeDuration regenerated = (aNow - mLastBudgetUpdate).MultDouble(factor);
377 // Clamp the budget to the range of minimum and maximum allowed budget.
378 mExecutionBudget = TimeDuration::Max(
379 GetMinBudget(isBackground),
380 TimeDuration::Min(GetMaxBudget(isBackground),
381 mExecutionBudget - aDuration + regenerated));
382 } else {
383 // If budget throttling isn't enabled, reset the execution budget
384 // to the max budget specified in preferences. Always doing this
385 // will catch the case of BudgetThrottlingEnabled going from
386 // returning true to returning false. This prevent us from looping
387 // in RunTimeout, due to totalTimeLimit being set to zero and no
388 // timeouts being executed, even though budget throttling isn't
389 // active at the moment.
390 mExecutionBudget = GetMaxBudget(isBackground);
393 mLastBudgetUpdate = aNow;
396 // The longest interval (as PRIntervalTime) we permit, or that our
397 // timer code can handle, really. See DELAY_INTERVAL_LIMIT in
398 // nsTimerImpl.h for details.
399 #define DOM_MAX_TIMEOUT_VALUE DELAY_INTERVAL_LIMIT
401 uint32_t TimeoutManager::sNestingLevel = 0;
403 TimeoutManager::TimeoutManager(nsIGlobalObject& aHandle,
404 uint32_t aMaxIdleDeferMS,
405 nsISerialEventTarget* aEventTarget)
406 : mGlobalObject(aHandle),
407 mExecutor(new TimeoutExecutor(this, false, 0)),
408 mIdleExecutor(new TimeoutExecutor(this, true, aMaxIdleDeferMS)),
409 mTimeouts(*this),
410 mTimeoutIdCounter(1),
411 mNextFiringId(InvalidFiringId + 1),
412 #ifdef DEBUG
413 mFiringIndex(0),
414 mLastFiringIndex(-1),
415 #endif
416 mRunningTimeout(nullptr),
417 mIdleTimeouts(*this),
418 mIdleCallbackTimeoutCounter(1),
419 mLastBudgetUpdate(TimeStamp::Now()),
420 mExecutionBudget(GetMaxBudget(mGlobalObject.IsBackgroundInternal())),
421 mThrottleTimeouts(false),
422 mThrottleTrackingTimeouts(false),
423 mBudgetThrottleTimeouts(false),
424 mIsLoading(false),
425 mEventTarget(aEventTarget) {
426 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
427 ("TimeoutManager %p created, tracking bucketing %s\n", this,
428 StaticPrefs::privacy_trackingprotection_annotate_channels()
429 ? "enabled"
430 : "disabled"));
433 TimeoutManager::~TimeoutManager() {
434 if (GetInnerWindow()) {
435 MOZ_DIAGNOSTIC_ASSERT(mGlobalObject.IsDying());
437 MOZ_DIAGNOSTIC_ASSERT(!mThrottleTimeoutsTimer);
439 mExecutor->Shutdown();
440 mIdleExecutor->Shutdown();
442 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
443 ("TimeoutManager %p destroyed\n", this));
446 int32_t TimeoutManager::GetTimeoutId(Timeout::Reason aReason) {
447 int32_t timeoutId;
448 do {
449 switch (aReason) {
450 case Timeout::Reason::eIdleCallbackTimeout:
451 timeoutId = mIdleCallbackTimeoutCounter;
452 if (mIdleCallbackTimeoutCounter ==
453 std::numeric_limits<int32_t>::max()) {
454 mIdleCallbackTimeoutCounter = 1;
455 } else {
456 ++mIdleCallbackTimeoutCounter;
458 break;
459 case Timeout::Reason::eTimeoutOrInterval:
460 timeoutId = mTimeoutIdCounter;
461 if (mTimeoutIdCounter == std::numeric_limits<int32_t>::max()) {
462 mTimeoutIdCounter = 1;
463 } else {
464 ++mTimeoutIdCounter;
466 break;
467 case Timeout::Reason::eDelayedWebTaskTimeout:
468 default:
469 return -1; // no cancellation support
471 } while (mTimeouts.GetTimeout(timeoutId, aReason));
473 return timeoutId;
476 bool TimeoutManager::IsRunningTimeout() const { return mRunningTimeout; }
478 nsresult TimeoutManager::SetTimeout(TimeoutHandler* aHandler, int32_t interval,
479 bool aIsInterval, Timeout::Reason aReason,
480 int32_t* aReturn) {
481 // If we don't have a document (we could have been unloaded since
482 // the call to setTimeout was made), do nothing.
483 if (mGlobalObject.GetAsInnerWindow()) {
484 nsCOMPtr<Document> doc = mGlobalObject.GetAsInnerWindow()->GetExtantDoc();
485 if (!doc || mGlobalObject.IsDying()) {
486 return NS_OK;
490 // Disallow negative intervals.
491 interval = std::max(0, interval);
493 // Make sure we don't proceed with an interval larger than our timer
494 // code can handle. (Note: we already forced |interval| to be non-negative,
495 // so the uint32_t cast (to avoid compiler warnings) is ok.)
496 uint32_t maxTimeoutMs = PR_IntervalToMilliseconds(DOM_MAX_TIMEOUT_VALUE);
497 if (static_cast<uint32_t>(interval) > maxTimeoutMs) {
498 interval = maxTimeoutMs;
501 RefPtr<Timeout> timeout = new Timeout();
502 #ifdef DEBUG
503 timeout->mFiringIndex = -1;
504 #endif
505 timeout->mGlobal = &mGlobalObject;
506 timeout->mIsInterval = aIsInterval;
507 timeout->mInterval = TimeDuration::FromMilliseconds(interval);
508 timeout->mScriptHandler = aHandler;
509 timeout->mReason = aReason;
511 // No popups from timeouts by default
512 timeout->mPopupState = PopupBlocker::openAbused;
514 // XXX: Does eIdleCallbackTimeout need clamping?
515 if (aReason == Timeout::Reason::eTimeoutOrInterval ||
516 aReason == Timeout::Reason::eIdleCallbackTimeout) {
517 const uint32_t nestingLevel{GetNestingLevel()};
518 timeout->mNestingLevel =
519 nestingLevel < StaticPrefs::dom_clamp_timeout_nesting_level_AtStartup()
520 ? nestingLevel + 1
521 : nestingLevel;
524 // Now clamp the actual interval we will use for the timer based on
525 TimeDuration realInterval = CalculateDelay(timeout);
526 TimeStamp now = TimeStamp::Now();
527 timeout->SetWhenOrTimeRemaining(now, realInterval);
529 // If we're not suspended, then set the timer.
530 if (!mGlobalObject.IsSuspended()) {
531 nsresult rv = MaybeSchedule(timeout->When(), now);
532 if (NS_FAILED(rv)) {
533 return rv;
537 if (gRunningTimeoutDepth == 0 &&
538 PopupBlocker::GetPopupControlState() < PopupBlocker::openBlocked) {
539 // This timeout is *not* set from another timeout and it's set
540 // while popups are enabled. Propagate the state to the timeout if
541 // its delay (interval) is equal to or less than what
542 // "dom.disable_open_click_delay" is set to (in ms).
544 // This is checking |interval|, not realInterval, on purpose,
545 // because our lower bound for |realInterval| could be pretty high
546 // in some cases.
547 if (interval <= StaticPrefs::dom_disable_open_click_delay()) {
548 timeout->mPopupState = PopupBlocker::GetPopupControlState();
552 Timeouts::SortBy sort(mGlobalObject.IsFrozen()
553 ? Timeouts::SortBy::TimeRemaining
554 : Timeouts::SortBy::TimeWhen);
556 timeout->mTimeoutId = GetTimeoutId(aReason);
557 mTimeouts.Insert(timeout, sort);
559 *aReturn = timeout->mTimeoutId;
561 MOZ_LOG(
562 gTimeoutLog, LogLevel::Debug,
563 ("Set%s(TimeoutManager=%p, timeout=%p, delay=%i, "
564 "minimum=%f, throttling=%s, state=%s(%s), realInterval=%f) "
565 "returned timeout ID %u, budget=%d\n",
566 aIsInterval ? "Interval" : "Timeout", this, timeout.get(), interval,
567 (CalculateDelay(timeout) - timeout->mInterval).ToMilliseconds(),
568 mThrottleTimeouts ? "yes" : (mThrottleTimeoutsTimer ? "pending" : "no"),
569 IsActive() ? "active" : "inactive",
570 mGlobalObject.IsBackgroundInternal() ? "background" : "foreground",
571 realInterval.ToMilliseconds(), timeout->mTimeoutId,
572 int(mExecutionBudget.ToMilliseconds())));
574 return NS_OK;
577 // Make sure we clear it no matter which list it's in
578 void TimeoutManager::ClearTimeout(int32_t aTimerId, Timeout::Reason aReason) {
579 if (ClearTimeoutInternal(aTimerId, aReason, false) ||
580 mIdleTimeouts.IsEmpty()) {
581 return; // no need to check the other list if we cleared the timeout
583 ClearTimeoutInternal(aTimerId, aReason, true);
586 bool TimeoutManager::ClearTimeoutInternal(int32_t aTimerId,
587 Timeout::Reason aReason,
588 bool aIsIdle) {
589 MOZ_ASSERT(aReason == Timeout::Reason::eTimeoutOrInterval ||
590 aReason == Timeout::Reason::eIdleCallbackTimeout,
591 "This timeout reason doesn't support cancellation.");
593 Timeouts& timeouts = aIsIdle ? mIdleTimeouts : mTimeouts;
594 RefPtr<TimeoutExecutor>& executor = aIsIdle ? mIdleExecutor : mExecutor;
595 bool deferredDeletion = false;
597 Timeout* timeout = timeouts.GetTimeout(aTimerId, aReason);
598 if (!timeout) {
599 return false;
601 bool firstTimeout = timeout == timeouts.GetFirst();
603 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
604 ("%s(TimeoutManager=%p, timeout=%p, ID=%u)\n",
605 timeout->mReason == Timeout::Reason::eIdleCallbackTimeout
606 ? "CancelIdleCallback"
607 : timeout->mIsInterval ? "ClearInterval"
608 : "ClearTimeout",
609 this, timeout, timeout->mTimeoutId));
611 if (timeout->mRunning) {
612 /* We're running from inside the timeout. Mark this
613 timeout for deferred deletion by the code in
614 RunTimeout() */
615 timeout->mIsInterval = false;
616 deferredDeletion = true;
617 } else {
618 /* Delete the aTimeout from the pending aTimeout list */
619 timeout->remove();
622 // We don't need to reschedule the executor if any of the following are true:
623 // * If the we weren't cancelling the first timeout, then the executor's
624 // state doesn't need to change. It will only reflect the next soonest
625 // Timeout.
626 // * If we did cancel the first Timeout, but its currently running, then
627 // RunTimeout() will handle rescheduling the executor.
628 // * If the window/worker has become suspended then we should not start
629 // executing
630 // Timeouts.
631 if (!firstTimeout || deferredDeletion || (mGlobalObject.IsSuspended())) {
632 return true;
635 // Stop the executor and restart it at the next soonest deadline.
636 executor->Cancel();
638 Timeout* nextTimeout = timeouts.GetFirst();
639 if (nextTimeout) {
640 if (aIsIdle) {
641 MOZ_ALWAYS_SUCCEEDS(
642 executor->MaybeSchedule(nextTimeout->When(), TimeDuration(0)));
643 } else {
644 MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextTimeout->When()));
647 return true;
650 void TimeoutManager::RunTimeout(const TimeStamp& aNow,
651 const TimeStamp& aTargetDeadline,
652 bool aProcessIdle) {
653 MOZ_DIAGNOSTIC_ASSERT(!aNow.IsNull());
654 MOZ_DIAGNOSTIC_ASSERT(!aTargetDeadline.IsNull());
656 nsCOMPtr<nsIGlobalObject> global = &mGlobalObject;
658 MOZ_ASSERT_IF(mGlobalObject.IsFrozen(), mGlobalObject.IsSuspended());
660 if (mGlobalObject.IsSuspended()) {
661 return;
664 Timeouts& timeouts(aProcessIdle ? mIdleTimeouts : mTimeouts);
666 // Limit the overall time spent in RunTimeout() to reduce jank.
667 uint32_t totalTimeLimitMS =
668 std::max(1u, StaticPrefs::dom_timeout_max_consecutive_callbacks_ms());
669 const TimeDuration totalTimeLimit =
670 TimeDuration::Min(TimeDuration::FromMilliseconds(totalTimeLimitMS),
671 TimeDuration::Max(TimeDuration(), mExecutionBudget));
673 // Allow up to 25% of our total time budget to be used figuring out which
674 // timers need to run. This is the initial loop in this method.
675 const TimeDuration initialTimeLimit =
676 TimeDuration::FromMilliseconds(totalTimeLimit.ToMilliseconds() / 4);
678 // Ammortize overhead from from calling TimeStamp::Now() in the initial
679 // loop, though, by only checking for an elapsed limit every N timeouts.
680 const uint32_t kNumTimersPerInitialElapsedCheck = 100;
682 // Start measuring elapsed time immediately. We won't potentially expire
683 // the time budget until at least one Timeout has run, though.
684 TimeStamp now(aNow);
685 TimeStamp start = now;
687 uint32_t firingId = CreateFiringId();
688 auto guard = MakeScopeExit([&] { DestroyFiringId(firingId); });
690 // Accessing members of mGlobalObject here is safe, because the lifetime of
691 // TimeoutManager is the same as the lifetime of the containing
692 // nsGlobalWindow.
694 // A native timer has gone off. See which of our timeouts need
695 // servicing
696 TimeStamp deadline;
698 if (aTargetDeadline > now) {
699 // The OS timer fired early (which can happen due to the timers
700 // having lower precision than TimeStamp does). Set |deadline| to
701 // be the time when the OS timer *should* have fired so that any
702 // timers that *should* have fired *will* be fired now.
704 deadline = aTargetDeadline;
705 } else {
706 deadline = now;
709 TimeStamp nextDeadline;
710 uint32_t numTimersToRun = 0;
712 // The timeout list is kept in deadline order. Discover the latest timeout
713 // whose deadline has expired. On some platforms, native timeout events fire
714 // "early", but we handled that above by setting deadline to aTargetDeadline
715 // if the timer fired early. So we can stop walking if we get to timeouts
716 // whose When() is greater than deadline, since once that happens we know
717 // nothing past that point is expired.
719 for (Timeout* timeout = timeouts.GetFirst(); timeout != nullptr;
720 timeout = timeout->getNext()) {
721 if (totalTimeLimit.IsZero() || timeout->When() > deadline) {
722 nextDeadline = timeout->When();
723 break;
726 if (IsInvalidFiringId(timeout->mFiringId)) {
727 // Mark any timeouts that are on the list to be fired with the
728 // firing depth so that we can reentrantly run timeouts
729 timeout->mFiringId = firingId;
731 numTimersToRun += 1;
733 // Run only a limited number of timers based on the configured maximum.
734 if (numTimersToRun % kNumTimersPerInitialElapsedCheck == 0) {
735 now = TimeStamp::Now();
736 TimeDuration elapsed(now - start);
737 if (elapsed >= initialTimeLimit) {
738 nextDeadline = timeout->When();
739 break;
744 if (aProcessIdle) {
745 MOZ_LOG(
746 gTimeoutLog, LogLevel::Debug,
747 ("Running %u deferred timeouts on idle (TimeoutManager=%p), "
748 "nextDeadline = %gms from now",
749 numTimersToRun, this,
750 nextDeadline.IsNull() ? 0.0 : (nextDeadline - now).ToMilliseconds()));
753 now = TimeStamp::Now();
755 // Wherever we stopped in the timer list, schedule the executor to
756 // run for the next unexpired deadline. Note, this *must* be done
757 // before we start executing any content script handlers. If one
758 // of them spins the event loop the executor must already be scheduled
759 // in order for timeouts to fire properly.
760 if (!nextDeadline.IsNull()) {
761 // Note, we verified the window/worker is not suspended at the top of
762 // method and the window/worker should not have been suspended while
763 // executing the loop above since it doesn't call out to js.
764 MOZ_DIAGNOSTIC_ASSERT(!mGlobalObject.IsSuspended());
765 if (aProcessIdle) {
766 // We don't want to update timing budget for idle queue firings, and
767 // all timeouts in the IdleTimeouts list have hit their deadlines,
768 // and so should run as soon as possible.
769 MOZ_ALWAYS_SUCCEEDS(
770 mIdleExecutor->MaybeSchedule(nextDeadline, TimeDuration()));
771 } else {
772 MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextDeadline, now));
776 // Maybe the timeout that the event was fired for has been deleted
777 // and there are no others timeouts with deadlines that make them
778 // eligible for execution yet. Go away.
779 if (!numTimersToRun) {
780 return;
783 // Now we need to search the normal and tracking timer list at the same
784 // time to run the timers in the scheduled order.
786 // We stop iterating each list when we go past the last expired timeout from
787 // that list that we have observed above. That timeout will either be the
788 // next item after the last timeout we looked at or nullptr if we have
789 // exhausted the entire list while looking for the last expired timeout.
791 // Use a nested scope in order to make sure the strong references held while
792 // iterating are freed after the loop.
794 // The next timeout to run. This is used to advance the loop, but
795 // we cannot set it until we've run the current timeout, since
796 // running the current timeout might remove the immediate next
797 // timeout.
798 RefPtr<Timeout> next;
800 for (RefPtr<Timeout> timeout = timeouts.GetFirst(); timeout != nullptr;
801 timeout = next) {
802 next = timeout->getNext();
803 // We should only execute callbacks for the set of expired Timeout
804 // objects we computed above.
805 if (timeout->mFiringId != firingId) {
806 // If the FiringId does not match, but is still valid, then this is
807 // a Timeout for another RunTimeout() on the call stack (such as in
808 // the case of nested event loops, for alert() or more likely XHR).
809 // Just skip it.
810 if (IsValidFiringId(timeout->mFiringId)) {
811 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
812 ("Skipping Run%s(TimeoutManager=%p, timeout=%p) since "
813 "firingId %d is valid (processing firingId %d)"
814 #ifdef DEBUG
815 " - FiringIndex %" PRId64 " (mLastFiringIndex %" PRId64 ")"
816 #endif
818 timeout->mIsInterval ? "Interval" : "Timeout", this,
819 timeout.get(), timeout->mFiringId, firingId
820 #ifdef DEBUG
822 timeout->mFiringIndex, mFiringIndex
823 #endif
825 #ifdef DEBUG
826 // The old FiringIndex assumed no recursion; recursion can cause
827 // other timers to get fired "in the middle" of a sequence we've
828 // already assigned firingindexes to. Since we're not going to
829 // run this timeout now, remove any FiringIndex that was already
830 // set.
832 // Since all timers that have FiringIndexes set *must* be ready
833 // to run and have valid FiringIds, all of them will be 'skipped'
834 // and reset if we recurse - we don't have to look through the
835 // list past where we'll stop on the first InvalidFiringId.
836 timeout->mFiringIndex = -1;
837 #endif
838 continue;
841 // If, however, the FiringId is invalid then we have reached Timeout
842 // objects beyond the list we calculated above. This can happen
843 // if the Timeout just beyond our last expired Timeout is cancelled
844 // by one of the callbacks we've just executed. In this case we
845 // should just stop iterating. We're done.
846 else {
847 break;
851 MOZ_ASSERT_IF(mGlobalObject.IsFrozen(), mGlobalObject.IsSuspended());
852 if (mGlobalObject.IsSuspended()) {
853 break;
856 // The timeout is on the list to run at this depth, go ahead and
857 // process it.
859 if (mIsLoading && !aProcessIdle) {
860 // Any timeouts that would fire during a load will be deferred
861 // until the load event occurs, but if there's an idle time,
862 // they'll be run before the load event.
863 timeout->remove();
864 // MOZ_RELEASE_ASSERT(timeout->When() <= (TimeStamp::Now()));
865 mIdleTimeouts.InsertBack(timeout);
866 if (MOZ_LOG_TEST(gTimeoutLog, LogLevel::Debug)) {
867 uint32_t num = 0;
868 for (Timeout* t = mIdleTimeouts.GetFirst(); t != nullptr;
869 t = t->getNext()) {
870 num++;
872 MOZ_LOG(
873 gTimeoutLog, LogLevel::Debug,
874 ("Deferring Run%s(TimeoutManager=%p, timeout=%p (%gms in the "
875 "past)) (%u deferred)",
876 timeout->mIsInterval ? "Interval" : "Timeout", this,
877 timeout.get(), (now - timeout->When()).ToMilliseconds(), num));
879 MOZ_ALWAYS_SUCCEEDS(mIdleExecutor->MaybeSchedule(now, TimeDuration()));
880 } else {
881 // Record the first time we try to fire a timeout, and ensure that
882 // all actual firings occur in that order. This ensures that we
883 // retain compliance with the spec language
884 // (https://html.spec.whatwg.org/#dom-settimeout) specifically items
885 // 15 ("If method context is a Window object, wait until the Document
886 // associated with method context has been fully active for a further
887 // timeout milliseconds (not necessarily consecutively)") and item 16
888 // ("Wait until any invocations of this algorithm that had the same
889 // method context, that started before this one, and whose timeout is
890 // equal to or less than this one's, have completed.").
891 #ifdef DEBUG
892 if (timeout->mFiringIndex == -1) {
893 timeout->mFiringIndex = mFiringIndex++;
895 #endif
897 if (mGlobalObject.IsDying()) {
898 timeout->remove();
899 continue;
902 #ifdef DEBUG
903 if (timeout->mFiringIndex <= mLastFiringIndex) {
904 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
905 ("Incorrect firing index for Run%s(TimeoutManager=%p, "
906 "timeout=%p) with "
907 "firingId %d - FiringIndex %" PRId64
908 " (mLastFiringIndex %" PRId64 ")",
909 timeout->mIsInterval ? "Interval" : "Timeout", this,
910 timeout.get(), timeout->mFiringId, timeout->mFiringIndex,
911 mFiringIndex));
913 MOZ_ASSERT(timeout->mFiringIndex > mLastFiringIndex);
914 mLastFiringIndex = timeout->mFiringIndex;
915 #endif
916 // This timeout is good to run.
917 bool timeout_was_cleared = false;
919 timeout_was_cleared = global->RunTimeoutHandler(timeout);
921 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
922 ("Run%s(TimeoutManager=%p, timeout=%p) returned %d\n",
923 timeout->mIsInterval ? "Interval" : "Timeout", this,
924 timeout.get(), !!timeout_was_cleared));
926 if (timeout_was_cleared) {
927 // Make sure we're not holding any Timeout objects alive.
928 next = nullptr;
930 // Since ClearAllTimeouts() was called the lists should be empty.
931 MOZ_DIAGNOSTIC_ASSERT(!HasTimeouts());
933 return;
936 // If we need to reschedule a setInterval() the delay should be
937 // calculated based on when its callback started to execute. So
938 // save off the last time before updating our "now" timestamp to
939 // account for its callback execution time.
940 TimeStamp lastCallbackTime = now;
941 now = TimeStamp::Now();
943 // If we have a regular interval timer, we re-schedule the
944 // timeout, accounting for clock drift.
945 bool needsReinsertion =
946 RescheduleTimeout(timeout, lastCallbackTime, now);
948 // Running a timeout can cause another timeout to be deleted, so
949 // we need to reset the pointer to the following timeout.
950 next = timeout->getNext();
952 timeout->remove();
954 if (needsReinsertion) {
955 // Insert interval timeout onto the corresponding list sorted in
956 // deadline order. AddRefs timeout.
957 // Always re-insert into the normal time queue!
958 mTimeouts.Insert(timeout, mGlobalObject.IsFrozen()
959 ? Timeouts::SortBy::TimeRemaining
960 : Timeouts::SortBy::TimeWhen);
963 // Check to see if we have run out of time to execute timeout handlers.
964 // If we've exceeded our time budget then terminate the loop immediately.
965 TimeDuration elapsed = now - start;
966 if (elapsed >= totalTimeLimit) {
967 // We ran out of time. Make sure to schedule the executor to
968 // run immediately for the next timer, if it exists. Its possible,
969 // however, that the last timeout handler suspended the window. If
970 // that happened then we must skip this step.
971 if (!mGlobalObject.IsSuspended()) {
972 if (next) {
973 if (aProcessIdle) {
974 // We don't want to update timing budget for idle queue firings,
975 // and all timeouts in the IdleTimeouts list have hit their
976 // deadlines, and so should run as soon as possible.
978 // Shouldn't need cancelling since it never waits
979 MOZ_ALWAYS_SUCCEEDS(
980 mIdleExecutor->MaybeSchedule(next->When(), TimeDuration()));
981 } else {
982 // If we ran out of execution budget we need to force a
983 // reschedule. By cancelling the executor we will not run
984 // immediately, but instead reschedule to the minimum
985 // scheduling delay.
986 if (mExecutionBudget < TimeDuration()) {
987 mExecutor->Cancel();
990 MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(next->When(), now));
994 break;
1000 bool TimeoutManager::RescheduleTimeout(Timeout* aTimeout,
1001 const TimeStamp& aLastCallbackTime,
1002 const TimeStamp& aCurrentNow) {
1003 MOZ_DIAGNOSTIC_ASSERT(aLastCallbackTime <= aCurrentNow);
1005 if (!aTimeout->mIsInterval) {
1006 return false;
1009 // Automatically increase the nesting level when a setInterval()
1010 // is rescheduled just as if it was using a chained setTimeout().
1011 if (aTimeout->mNestingLevel <
1012 StaticPrefs::dom_clamp_timeout_nesting_level_AtStartup()) {
1013 aTimeout->mNestingLevel += 1;
1016 // Compute time to next timeout for interval timer.
1017 // Make sure nextInterval is at least CalculateDelay().
1018 TimeDuration nextInterval = CalculateDelay(aTimeout);
1020 TimeStamp firingTime = aLastCallbackTime + nextInterval;
1021 TimeDuration delay = firingTime - aCurrentNow;
1023 #ifdef DEBUG
1024 aTimeout->mFiringIndex = -1;
1025 #endif
1026 // And make sure delay is nonnegative; that might happen if the timer
1027 // thread is firing our timers somewhat early or if they're taking a long
1028 // time to run the callback.
1029 if (delay < TimeDuration(0)) {
1030 delay = TimeDuration(0);
1033 aTimeout->SetWhenOrTimeRemaining(aCurrentNow, delay);
1035 if (mGlobalObject.IsSuspended()) {
1036 return true;
1039 nsresult rv = MaybeSchedule(aTimeout->When(), aCurrentNow);
1040 NS_ENSURE_SUCCESS(rv, false);
1042 return true;
1045 void TimeoutManager::ClearAllTimeouts() {
1046 bool seenRunningTimeout = false;
1048 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
1049 ("ClearAllTimeouts(TimeoutManager=%p)\n", this));
1051 if (mThrottleTimeoutsTimer) {
1052 mThrottleTimeoutsTimer->Cancel();
1053 mThrottleTimeoutsTimer = nullptr;
1056 mExecutor->Cancel();
1057 mIdleExecutor->Cancel();
1059 ForEachUnorderedTimeout([&](Timeout* aTimeout) {
1060 /* If RunTimeout() is higher up on the stack for this
1061 window, e.g. as a result of document.write from a timeout,
1062 then we need to reset the list insertion point for
1063 newly-created timeouts in case the user adds a timeout,
1064 before we pop the stack back to RunTimeout. */
1065 if (mRunningTimeout == aTimeout) {
1066 seenRunningTimeout = true;
1069 // Set timeout->mCleared to true to indicate that the timeout was
1070 // cleared and taken out of the list of timeouts
1071 aTimeout->mCleared = true;
1074 // Clear out our lists
1075 mTimeouts.Clear();
1076 mIdleTimeouts.Clear();
1079 void TimeoutManager::Timeouts::Insert(Timeout* aTimeout, SortBy aSortBy) {
1080 // Start at mLastTimeout and go backwards. Stop if we see a Timeout with a
1081 // valid FiringId since those timers are currently being processed by
1082 // RunTimeout. This optimizes for the common case of insertion at the end.
1083 Timeout* prevSibling;
1084 for (prevSibling = GetLast();
1085 prevSibling &&
1086 // This condition needs to match the one in SetTimeoutOrInterval that
1087 // determines whether to set When() or TimeRemaining().
1088 (aSortBy == SortBy::TimeRemaining
1089 ? prevSibling->TimeRemaining() > aTimeout->TimeRemaining()
1090 : prevSibling->When() > aTimeout->When()) &&
1091 // Check the firing ID last since it will evaluate true in the vast
1092 // majority of cases.
1093 mManager.IsInvalidFiringId(prevSibling->mFiringId);
1094 prevSibling = prevSibling->getPrevious()) {
1095 /* Do nothing; just searching */
1098 // Now link in aTimeout after prevSibling.
1099 if (prevSibling) {
1100 aTimeout->SetTimeoutContainer(mTimeouts);
1101 prevSibling->setNext(aTimeout);
1102 } else {
1103 InsertFront(aTimeout);
1106 aTimeout->mFiringId = InvalidFiringId;
1109 Timeout* TimeoutManager::BeginRunningTimeout(Timeout* aTimeout) {
1110 Timeout* currentTimeout = mRunningTimeout;
1111 mRunningTimeout = aTimeout;
1112 ++gRunningTimeoutDepth;
1114 RecordExecution(currentTimeout, aTimeout);
1115 return currentTimeout;
1118 void TimeoutManager::EndRunningTimeout(Timeout* aTimeout) {
1119 --gRunningTimeoutDepth;
1121 RecordExecution(mRunningTimeout, aTimeout);
1122 mRunningTimeout = aTimeout;
1125 void TimeoutManager::UnmarkGrayTimers() {
1126 ForEachUnorderedTimeout([](Timeout* aTimeout) {
1127 if (aTimeout->mScriptHandler) {
1128 aTimeout->mScriptHandler->MarkForCC();
1133 void TimeoutManager::Suspend() {
1134 MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Suspend(TimeoutManager=%p)\n", this));
1136 if (mThrottleTimeoutsTimer) {
1137 mThrottleTimeoutsTimer->Cancel();
1138 mThrottleTimeoutsTimer = nullptr;
1141 mExecutor->Cancel();
1142 mIdleExecutor->Cancel();
1145 void TimeoutManager::Resume() {
1146 MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Resume(TimeoutManager=%p)\n", this));
1147 nsGlobalWindowInner* window = GetInnerWindow();
1149 // When Suspend() has been called after IsDocumentLoaded(), but the
1150 // throttle tracking timer never managed to fire, start the timer
1151 // again.
1152 if (window && window->IsDocumentLoaded() && !mThrottleTimeouts) {
1153 MaybeStartThrottleTimeout();
1156 Timeout* nextTimeout = mTimeouts.GetFirst();
1157 if (nextTimeout) {
1158 MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextTimeout->When()));
1160 nextTimeout = mIdleTimeouts.GetFirst();
1161 if (nextTimeout) {
1162 MOZ_ALWAYS_SUCCEEDS(
1163 mIdleExecutor->MaybeSchedule(nextTimeout->When(), TimeDuration()));
1167 void TimeoutManager::Freeze() {
1168 MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Freeze(TimeoutManager=%p)\n", this));
1170 // When freezing, preemptively move timeouts from the idle timeout queue to
1171 // the normal queue. This way they get scheduled automatically when we thaw.
1172 // We don't need to cancel the idle executor here, since that is done in
1173 // Suspend.
1174 size_t num = 0;
1175 while (RefPtr<Timeout> timeout = mIdleTimeouts.GetLast()) {
1176 num++;
1177 timeout->remove();
1178 mTimeouts.InsertFront(timeout);
1181 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
1182 ("%p: Moved %zu (frozen) timeouts from Idle to active", this, num));
1184 TimeStamp now = TimeStamp::Now();
1185 ForEachUnorderedTimeout([&](Timeout* aTimeout) {
1186 // Save the current remaining time for this timeout. We will
1187 // re-apply it when the window is Thaw()'d. This effectively
1188 // shifts timers to the right as if time does not pass while
1189 // the window is frozen.
1190 TimeDuration delta(0);
1191 if (aTimeout->When() > now) {
1192 delta = aTimeout->When() - now;
1194 aTimeout->SetWhenOrTimeRemaining(now, delta);
1195 MOZ_DIAGNOSTIC_ASSERT(aTimeout->TimeRemaining() == delta);
1199 void TimeoutManager::Thaw() {
1200 MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Thaw(TimeoutManager=%p)\n", this));
1202 TimeStamp now = TimeStamp::Now();
1204 ForEachUnorderedTimeout([&](Timeout* aTimeout) {
1205 // Set When() back to the time when the timer is supposed to fire.
1206 aTimeout->SetWhenOrTimeRemaining(now, aTimeout->TimeRemaining());
1207 MOZ_DIAGNOSTIC_ASSERT(!aTimeout->When().IsNull());
1211 void TimeoutManager::UpdateBackgroundState() {
1212 mExecutionBudget = GetMaxBudget(mGlobalObject.IsBackgroundInternal());
1214 // When the window/worker moves to the background or foreground we should
1215 // reschedule the TimeoutExecutor in case the MinSchedulingDelay()
1216 // changed. Only do this if the window/worker is not suspended and we
1217 // actually have a timeout.
1218 if (!mGlobalObject.IsSuspended()) {
1219 Timeout* nextTimeout = mTimeouts.GetFirst();
1220 if (nextTimeout) {
1221 mExecutor->Cancel();
1222 MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextTimeout->When()));
1224 // the Idle queue should all be past their firing time, so there we just
1225 // need to restart the queue
1227 // XXX May not be needed if we don't stop the idle queue, as
1228 // MinSchedulingDelay isn't relevant here
1229 nextTimeout = mIdleTimeouts.GetFirst();
1230 if (nextTimeout) {
1231 mIdleExecutor->Cancel();
1232 MOZ_ALWAYS_SUCCEEDS(
1233 mIdleExecutor->MaybeSchedule(nextTimeout->When(), TimeDuration()));
1238 namespace {
1240 class ThrottleTimeoutsCallback final : public nsITimerCallback,
1241 public nsINamed {
1242 public:
1243 explicit ThrottleTimeoutsCallback(nsIGlobalObject* aHandle)
1244 : mGlobalObject(aHandle) {}
1246 NS_DECL_ISUPPORTS
1247 NS_DECL_NSITIMERCALLBACK
1249 NS_IMETHOD GetName(nsACString& aName) override {
1250 aName.AssignLiteral("ThrottleTimeoutsCallback");
1251 return NS_OK;
1254 private:
1255 ~ThrottleTimeoutsCallback() = default;
1257 private:
1258 // The strong reference here keeps the Window/worker and hence the
1259 // TimeoutManager object itself alive.
1260 RefPtr<nsIGlobalObject> mGlobalObject;
1263 NS_IMPL_ISUPPORTS(ThrottleTimeoutsCallback, nsITimerCallback, nsINamed)
1265 NS_IMETHODIMP
1266 ThrottleTimeoutsCallback::Notify(nsITimer* aTimer) {
1267 if (mGlobalObject) {
1268 mGlobalObject->GetTimeoutManager()->StartThrottlingTimeouts();
1270 mGlobalObject = nullptr;
1271 return NS_OK;
1274 } // namespace
1276 bool TimeoutManager::BudgetThrottlingEnabled(bool aIsBackground) const {
1277 // do not throttle workers if dom_workers_throttling is disabled
1278 if (!GetInnerWindow() && StaticPrefs::dom_workers_timeoutmanager() &&
1279 !StaticPrefs::dom_workers_throttling_enabled()) {
1280 return false;
1283 // A window/worker can be throttled using budget if
1284 // * It isn't active
1285 // * If it isn't using WebRTC
1286 // * If it hasn't got open WebSockets
1287 // * If it hasn't got active IndexedDB databases
1289 // Note that we allow both foreground and background to be
1290 // considered for budget throttling. What determines if they are if
1291 // budget throttling is enabled is the max budget.
1292 if ((aIsBackground
1293 ? StaticPrefs::dom_timeout_background_throttling_max_budget()
1294 : StaticPrefs::dom_timeout_foreground_throttling_max_budget()) < 0) {
1295 return false;
1298 if (!mBudgetThrottleTimeouts || IsActive()) {
1299 return false;
1302 // Check if there are any active IndexedDB databases
1303 // TODO: mGlobalObject must implement HasActiveIndexedDBDatabases()
1304 // Not implemented yet in workers
1305 if (mGlobalObject.HasActiveIndexedDBDatabases()) {
1306 // TODO: A window/worker can be throttled using budget if mGlobalObject has
1307 // active IndexedDB Databases
1308 // Not implemented yet in workers
1309 return false;
1312 // Check if we have active PeerConnection
1313 // TODO: mGlobalObject must implement HasActivePeerConnections()
1314 if (mGlobalObject.HasActivePeerConnections()) {
1315 // TODO: A window/worker can be throttled using budget if mGlobalObject has
1316 // active peer connections
1317 // Not implemented yet in workers
1318 return false;
1321 if (mGlobalObject.HasOpenWebSockets()) {
1322 // TODO: A window/worker can be throttled using budget if mGlobalObject has
1323 // open web sockets
1324 // Not implemented yet in workers
1325 return false;
1328 return true;
1331 void TimeoutManager::StartThrottlingTimeouts() {
1332 MOZ_ASSERT(NS_IsMainThread());
1333 MOZ_DIAGNOSTIC_ASSERT(mThrottleTimeoutsTimer);
1335 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
1336 ("TimeoutManager %p started to throttle tracking timeouts\n", this));
1338 MOZ_DIAGNOSTIC_ASSERT(!mThrottleTimeouts);
1339 mThrottleTimeouts = true;
1340 mThrottleTrackingTimeouts = true;
1341 mBudgetThrottleTimeouts =
1342 StaticPrefs::dom_timeout_enable_budget_timer_throttling();
1343 mThrottleTimeoutsTimer = nullptr;
1346 void TimeoutManager::OnDocumentLoaded() {
1347 // The load event may be firing again if we're coming back to the page by
1348 // navigating through the session history, so we need to ensure to only call
1349 // this when mThrottleTimeouts hasn't been set yet.
1350 if (!mThrottleTimeouts) {
1351 MaybeStartThrottleTimeout();
1355 void TimeoutManager::MaybeStartThrottleTimeout() {
1356 if (StaticPrefs::dom_timeout_throttling_delay() <= 0 ||
1357 mGlobalObject.IsDying() || mGlobalObject.IsSuspended()) {
1358 return;
1361 MOZ_DIAGNOSTIC_ASSERT(!mThrottleTimeouts);
1363 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
1364 ("TimeoutManager %p delaying tracking timeout throttling by %dms\n",
1365 this, StaticPrefs::dom_timeout_throttling_delay()));
1367 nsCOMPtr<nsITimerCallback> callback =
1368 new ThrottleTimeoutsCallback(&mGlobalObject);
1370 NS_NewTimerWithCallback(getter_AddRefs(mThrottleTimeoutsTimer), callback,
1371 StaticPrefs::dom_timeout_throttling_delay(),
1372 nsITimer::TYPE_ONE_SHOT, EventTarget());
1375 void TimeoutManager::BeginSyncOperation() {
1376 // If we're beginning a sync operation, the currently running
1377 // timeout will be put on hold. To not get into an inconsistent
1378 // state, where the currently running timeout appears to take time
1379 // equivalent to the period of us spinning up a new event loop,
1380 // record what we have and stop recording until we reach
1381 // EndSyncOperation.
1382 RecordExecution(mRunningTimeout, nullptr);
1385 void TimeoutManager::EndSyncOperation() {
1386 // If we're running a timeout, restart the measurement from here.
1387 RecordExecution(nullptr, mRunningTimeout);
1390 nsIEventTarget* TimeoutManager::EventTarget() { return mEventTarget; }