Bug 1550519 - Show a translucent parent highlight when a subgrid is highlighted....
[gecko.git] / dom / base / TimeoutManager.cpp
blobb6d2e850dbd6fbd69540964cb4c7ce94215bec1b
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 "nsGlobalWindow.h"
9 #include "mozilla/Logging.h"
10 #include "mozilla/PerformanceCounter.h"
11 #include "mozilla/StaticPrefs.h"
12 #include "mozilla/Telemetry.h"
13 #include "mozilla/ThrottledEventQueue.h"
14 #include "mozilla/TimeStamp.h"
15 #include "nsIDocShell.h"
16 #include "nsINamed.h"
17 #include "nsITimeoutHandler.h"
18 #include "mozilla/dom/DocGroup.h"
19 #include "mozilla/dom/PopupBlocker.h"
20 #include "mozilla/dom/TabGroup.h"
21 #include "TimeoutExecutor.h"
22 #include "TimeoutBudgetManager.h"
23 #include "mozilla/net/WebSocketEventService.h"
24 #include "mozilla/MediaManager.h"
25 #ifdef MOZ_GECKO_PROFILER
26 # include "ProfilerMarkerPayload.h"
27 #endif
29 using namespace mozilla;
30 using namespace mozilla::dom;
32 LazyLogModule gTimeoutLog("Timeout");
34 static int32_t gRunningTimeoutDepth = 0;
36 // The default shortest interval/timeout we permit
37 #define DEFAULT_MIN_CLAMP_TIMEOUT_VALUE 4 // 4ms
38 #define DEFAULT_MIN_BACKGROUND_TIMEOUT_VALUE 1000 // 1000ms
39 #define DEFAULT_MIN_TRACKING_TIMEOUT_VALUE 4 // 4ms
40 #define DEFAULT_MIN_TRACKING_BACKGROUND_TIMEOUT_VALUE 1000 // 1000ms
41 static int32_t gMinClampTimeoutValue = 0;
42 static int32_t gMinBackgroundTimeoutValue = 0;
43 static int32_t gMinTrackingTimeoutValue = 0;
44 static int32_t gMinTrackingBackgroundTimeoutValue = 0;
45 static int32_t gTimeoutThrottlingDelay = 0;
47 #define DEFAULT_BACKGROUND_BUDGET_REGENERATION_FACTOR 100 // 1ms per 100ms
48 #define DEFAULT_FOREGROUND_BUDGET_REGENERATION_FACTOR 1 // 1ms per 1ms
49 #define DEFAULT_BACKGROUND_THROTTLING_MAX_BUDGET 50 // 50ms
50 #define DEFAULT_FOREGROUND_THROTTLING_MAX_BUDGET -1 // infinite
51 #define DEFAULT_BUDGET_THROTTLING_MAX_DELAY 15000 // 15s
52 #define DEFAULT_ENABLE_BUDGET_TIMEOUT_THROTTLING false
53 static int32_t gBackgroundBudgetRegenerationFactor = 0;
54 static int32_t gForegroundBudgetRegenerationFactor = 0;
55 static int32_t gBackgroundThrottlingMaxBudget = 0;
56 static int32_t gForegroundThrottlingMaxBudget = 0;
57 static int32_t gBudgetThrottlingMaxDelay = 0;
58 static bool gEnableBudgetTimeoutThrottling = false;
60 // static
61 const uint32_t TimeoutManager::InvalidFiringId = 0;
63 namespace {
64 double GetRegenerationFactor(bool aIsBackground) {
65 // Lookup function for "dom.timeout.{background,
66 // foreground}_budget_regeneration_rate".
68 // Returns the rate of regeneration of the execution budget as a
69 // fraction. If the value is 1.0, the amount of time regenerated is
70 // equal to time passed. At this rate we regenerate 1ms/ms. If it is
71 // 0.01 the amount regenerated is 1% of time passed. At this rate we
72 // regenerate 1ms/100ms, etc.
73 double denominator =
74 std::max(aIsBackground ? gBackgroundBudgetRegenerationFactor
75 : gForegroundBudgetRegenerationFactor,
76 1);
77 return 1.0 / denominator;
80 TimeDuration GetMaxBudget(bool aIsBackground) {
81 // Lookup function for "dom.timeout.{background,
82 // foreground}_throttling_max_budget".
84 // Returns how high a budget can be regenerated before being
85 // clamped. If this value is less or equal to zero,
86 // TimeDuration::Forever() is implied.
87 int32_t maxBudget = aIsBackground ? gBackgroundThrottlingMaxBudget
88 : gForegroundThrottlingMaxBudget;
89 return maxBudget > 0 ? TimeDuration::FromMilliseconds(maxBudget)
90 : TimeDuration::Forever();
93 TimeDuration GetMinBudget(bool aIsBackground) {
94 // The minimum budget is computed by looking up the maximum allowed
95 // delay and computing how long time it would take to regenerate
96 // that budget using the regeneration factor. This number is
97 // expected to be negative.
98 return TimeDuration::FromMilliseconds(
99 -gBudgetThrottlingMaxDelay /
100 std::max(aIsBackground ? gBackgroundBudgetRegenerationFactor
101 : gForegroundBudgetRegenerationFactor,
102 1));
104 } // namespace
108 bool TimeoutManager::IsBackground() const {
109 return !IsActive() && mWindow.IsBackgroundInternal();
112 bool TimeoutManager::IsActive() const {
113 // A window is considered active if:
114 // * It is a chrome window
115 // * It is playing audio
117 // Note that a window can be considered active if it is either in the
118 // foreground or in the background.
120 if (mWindow.IsChromeWindow()) {
121 return true;
124 // Check if we're playing audio
125 if (mWindow.IsPlayingAudio()) {
126 return true;
129 return false;
132 void TimeoutManager::SetLoading(bool value) {
133 // When moving from loading to non-loading, we may need to
134 // reschedule any existing timeouts from the idle timeout queue
135 // to the normal queue.
136 MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("%p: SetLoading(%d)", this, value));
137 if (mIsLoading && !value) {
138 MoveIdleToActive();
140 // We don't immediately move existing timeouts to the idle queue if we
141 // move to loading. When they would have fired, we'll see we're loading
142 // and move them then.
143 mIsLoading = value;
146 void TimeoutManager::MoveIdleToActive() {
147 uint32_t num = 0;
148 TimeStamp when;
149 #if MOZ_GECKO_PROFILER
150 TimeStamp now;
151 #endif
152 // Ensure we maintain the ordering of timeouts, so timeouts
153 // never fire before a timeout set for an earlier time, or
154 // before a timeout for the same time already submitted.
155 // See https://html.spec.whatwg.org/#dom-settimeout #16 and #17
156 while (RefPtr<Timeout> timeout = mIdleTimeouts.GetLast()) {
157 if (num == 0) {
158 when = timeout->When();
160 timeout->remove();
161 mTimeouts.InsertFront(timeout);
162 #if MOZ_GECKO_PROFILER
163 if (profiler_is_active()) {
164 if (num == 0) {
165 now = TimeStamp::Now();
167 TimeDuration elapsed = now - timeout->SubmitTime();
168 TimeDuration target = timeout->When() - timeout->SubmitTime();
169 TimeDuration delta = now - timeout->When();
170 nsPrintfCString marker(
171 "Releasing deferred setTimeout() for %dms (original target time was "
172 "%dms (%dms delta))",
173 int(elapsed.ToMilliseconds()), int(target.ToMilliseconds()),
174 int(delta.ToMilliseconds()));
175 // don't have end before start...
176 profiler_add_marker(
177 "setTimeout deferred release", JS::ProfilingCategoryPair::DOM,
178 MakeUnique<TextMarkerPayload>(
179 marker, delta.ToMilliseconds() >= 0 ? timeout->When() : now,
180 now));
182 #endif
183 num++;
185 if (num > 0) {
186 MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(when));
187 mIdleExecutor->Cancel();
189 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
190 ("%p: Moved %d timeouts from Idle to active", this, num));
193 uint32_t TimeoutManager::CreateFiringId() {
194 uint32_t id = mNextFiringId;
195 mNextFiringId += 1;
196 if (mNextFiringId == InvalidFiringId) {
197 mNextFiringId += 1;
200 mFiringIdStack.AppendElement(id);
202 return id;
205 void TimeoutManager::DestroyFiringId(uint32_t aFiringId) {
206 MOZ_DIAGNOSTIC_ASSERT(!mFiringIdStack.IsEmpty());
207 MOZ_DIAGNOSTIC_ASSERT(mFiringIdStack.LastElement() == aFiringId);
208 mFiringIdStack.RemoveLastElement();
211 bool TimeoutManager::IsValidFiringId(uint32_t aFiringId) const {
212 return !IsInvalidFiringId(aFiringId);
215 TimeDuration TimeoutManager::MinSchedulingDelay() const {
216 if (IsActive()) {
217 return TimeDuration();
220 bool isBackground = mWindow.IsBackgroundInternal();
222 // If a window isn't active as defined by TimeoutManager::IsActive()
223 // and we're throttling timeouts using an execution budget, we
224 // should adjust the minimum scheduling delay if we have used up all
225 // of our execution budget. Note that a window can be active or
226 // inactive regardless of wether it is in the foreground or in the
227 // background. Throttling using a budget depends largely on the
228 // regeneration factor, which can be specified separately for
229 // foreground and background windows.
231 // The value that we compute is the time in the future when we again
232 // have a positive execution budget. We do this by taking the
233 // execution budget into account, which if it positive implies that
234 // we have time left to execute, and if it is negative implies that
235 // we should throttle it until the budget again is positive. The
236 // factor used is the rate of budget regeneration.
238 // We clamp the delay to be less than or equal to
239 // gBudgetThrottlingMaxDelay to not entirely starve the timeouts.
241 // Consider these examples assuming we should throttle using
242 // budgets:
244 // mExecutionBudget is 20ms
245 // factor is 1, which is 1 ms/ms
246 // delay is 0ms
247 // then we will compute the minimum delay:
248 // max(0, - 20 * 1) = 0
250 // mExecutionBudget is -50ms
251 // factor is 0.1, which is 1 ms/10ms
252 // delay is 1000ms
253 // then we will compute the minimum delay:
254 // max(1000, - (- 50) * 1/0.1) = max(1000, 500) = 1000
256 // mExecutionBudget is -15ms
257 // factor is 0.01, which is 1 ms/100ms
258 // delay is 1000ms
259 // then we will compute the minimum delay:
260 // max(1000, - (- 15) * 1/0.01) = max(1000, 1500) = 1500
261 TimeDuration unthrottled =
262 isBackground ? TimeDuration::FromMilliseconds(gMinBackgroundTimeoutValue)
263 : TimeDuration();
264 if (BudgetThrottlingEnabled(isBackground) &&
265 mExecutionBudget < TimeDuration()) {
266 // Only throttle if execution budget is less than 0
267 double factor = 1.0 / GetRegenerationFactor(mWindow.IsBackgroundInternal());
268 return TimeDuration::Max(unthrottled, -mExecutionBudget.MultDouble(factor));
271 return unthrottled;
274 nsresult TimeoutManager::MaybeSchedule(const TimeStamp& aWhen,
275 const TimeStamp& aNow) {
276 MOZ_DIAGNOSTIC_ASSERT(mExecutor);
278 // Before we can schedule the executor we need to make sure that we
279 // have an updated execution budget.
280 UpdateBudget(aNow);
281 return mExecutor->MaybeSchedule(aWhen, MinSchedulingDelay());
284 bool TimeoutManager::IsInvalidFiringId(uint32_t aFiringId) const {
285 // Check the most common ways to invalidate a firing id first.
286 // These should be quite fast.
287 if (aFiringId == InvalidFiringId || mFiringIdStack.IsEmpty()) {
288 return true;
291 if (mFiringIdStack.Length() == 1) {
292 return mFiringIdStack[0] != aFiringId;
295 // Next do a range check on the first and last items in the stack
296 // of active firing ids. This is a bit slower.
297 uint32_t low = mFiringIdStack[0];
298 uint32_t high = mFiringIdStack.LastElement();
299 MOZ_DIAGNOSTIC_ASSERT(low != high);
300 if (low > high) {
301 // If the first element is bigger than the last element in the
302 // stack, that means mNextFiringId wrapped around to zero at
303 // some point.
304 Swap(low, high);
306 MOZ_DIAGNOSTIC_ASSERT(low < high);
308 if (aFiringId < low || aFiringId > high) {
309 return true;
312 // Finally, fall back to verifying the firing id is not anywhere
313 // in the stack. This could be slow for a large stack, but that
314 // should be rare. It can only happen with deeply nested event
315 // loop spinning. For example, a page that does a lot of timers
316 // and a lot of sync XHRs within those timers could be slow here.
317 return !mFiringIdStack.Contains(aFiringId);
320 // The number of nested timeouts before we start clamping. HTML5 says 1, WebKit
321 // uses 5.
322 #define DOM_CLAMP_TIMEOUT_NESTING_LEVEL 5u
324 TimeDuration TimeoutManager::CalculateDelay(Timeout* aTimeout) const {
325 MOZ_DIAGNOSTIC_ASSERT(aTimeout);
326 TimeDuration result = aTimeout->mInterval;
328 if (aTimeout->mNestingLevel >= DOM_CLAMP_TIMEOUT_NESTING_LEVEL) {
329 result = TimeDuration::Max(
330 result, TimeDuration::FromMilliseconds(gMinClampTimeoutValue));
333 return result;
336 PerformanceCounter* TimeoutManager::GetPerformanceCounter() {
337 Document* doc = mWindow.GetDocument();
338 if (doc) {
339 dom::DocGroup* docGroup = doc->GetDocGroup();
340 if (docGroup) {
341 return docGroup->GetPerformanceCounter();
344 return nullptr;
347 void TimeoutManager::RecordExecution(Timeout* aRunningTimeout,
348 Timeout* aTimeout) {
349 TimeoutBudgetManager& budgetManager = TimeoutBudgetManager::Get();
350 TimeStamp now = TimeStamp::Now();
352 if (aRunningTimeout) {
353 // If we're running a timeout callback, record any execution until
354 // now.
355 TimeDuration duration = budgetManager.RecordExecution(now, aRunningTimeout);
357 UpdateBudget(now, duration);
359 // This is an ad-hoc way to use the counters for the timers
360 // that should be removed at somepoint. See Bug 1482834
361 PerformanceCounter* counter = GetPerformanceCounter();
362 if (counter) {
363 counter->IncrementExecutionDuration(duration.ToMicroseconds());
367 if (aTimeout) {
368 // If we're starting a new timeout callback, start recording.
369 budgetManager.StartRecording(now);
370 PerformanceCounter* counter = GetPerformanceCounter();
371 if (counter) {
372 counter->IncrementDispatchCounter(DispatchCategory(TaskCategory::Timer));
374 } else {
375 // Else stop by clearing the start timestamp.
376 budgetManager.StopRecording();
380 void TimeoutManager::UpdateBudget(const TimeStamp& aNow,
381 const TimeDuration& aDuration) {
382 if (mWindow.IsChromeWindow()) {
383 return;
386 // The budget is adjusted by increasing it with the time since the
387 // last budget update factored with the regeneration rate. If a
388 // runnable has executed, subtract that duration from the
389 // budget. The budget updated without consideration of wether the
390 // window is active or not. If throttling is enabled and the window
391 // is active and then becomes inactive, an overdrawn budget will
392 // still be counted against the minimum delay.
393 bool isBackground = mWindow.IsBackgroundInternal();
394 if (BudgetThrottlingEnabled(isBackground)) {
395 double factor = GetRegenerationFactor(isBackground);
396 TimeDuration regenerated = (aNow - mLastBudgetUpdate).MultDouble(factor);
397 // Clamp the budget to the range of minimum and maximum allowed budget.
398 mExecutionBudget = TimeDuration::Max(
399 GetMinBudget(isBackground),
400 TimeDuration::Min(GetMaxBudget(isBackground),
401 mExecutionBudget - aDuration + regenerated));
402 } else {
403 // If budget throttling isn't enabled, reset the execution budget
404 // to the max budget specified in preferences. Always doing this
405 // will catch the case of BudgetThrottlingEnabled going from
406 // returning true to returning false. This prevent us from looping
407 // in RunTimeout, due to totalTimeLimit being set to zero and no
408 // timeouts being executed, even though budget throttling isn't
409 // active at the moment.
410 mExecutionBudget = GetMaxBudget(isBackground);
413 mLastBudgetUpdate = aNow;
416 #define DEFAULT_TIMEOUT_THROTTLING_DELAY \
417 -1 // Only positive integers cause us to introduce a delay for
418 // timeout throttling.
420 // The longest interval (as PRIntervalTime) we permit, or that our
421 // timer code can handle, really. See DELAY_INTERVAL_LIMIT in
422 // nsTimerImpl.h for details.
423 #define DOM_MAX_TIMEOUT_VALUE DELAY_INTERVAL_LIMIT
425 uint32_t TimeoutManager::sNestingLevel = 0;
427 namespace {
429 // The maximum number of milliseconds to allow consecutive timer callbacks
430 // to run in a single event loop runnable.
431 #define DEFAULT_MAX_CONSECUTIVE_CALLBACKS_MILLISECONDS 4
432 uint32_t gMaxConsecutiveCallbacksMilliseconds;
434 // Only propagate the open window click permission if the setTimeout() is equal
435 // to or less than this value.
436 #define DEFAULT_DISABLE_OPEN_CLICK_DELAY 0
437 int32_t gDisableOpenClickDelay;
439 } // anonymous namespace
441 TimeoutManager::TimeoutManager(nsGlobalWindowInner& aWindow,
442 uint32_t aMaxIdleDeferMS)
443 : mWindow(aWindow),
444 mExecutor(new TimeoutExecutor(this, false, 0)),
445 mIdleExecutor(new TimeoutExecutor(this, true, aMaxIdleDeferMS)),
446 mTimeouts(*this),
447 mTimeoutIdCounter(1),
448 mNextFiringId(InvalidFiringId + 1),
449 #ifdef DEBUG
450 mFiringIndex(0),
451 mLastFiringIndex(-1),
452 #endif
453 mRunningTimeout(nullptr),
454 mIdleTimeouts(*this),
455 mIdleCallbackTimeoutCounter(1),
456 mLastBudgetUpdate(TimeStamp::Now()),
457 mExecutionBudget(GetMaxBudget(mWindow.IsBackgroundInternal())),
458 mThrottleTimeouts(false),
459 mThrottleTrackingTimeouts(false),
460 mBudgetThrottleTimeouts(false),
461 mIsLoading(false) {
462 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
463 ("TimeoutManager %p created, tracking bucketing %s\n", this,
464 StaticPrefs::privacy_trackingprotection_annotate_channels()
465 ? "enabled"
466 : "disabled"));
469 TimeoutManager::~TimeoutManager() {
470 MOZ_DIAGNOSTIC_ASSERT(mWindow.IsDying());
471 MOZ_DIAGNOSTIC_ASSERT(!mThrottleTimeoutsTimer);
473 mExecutor->Shutdown();
474 mIdleExecutor->Shutdown();
476 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
477 ("TimeoutManager %p destroyed\n", this));
480 /* static */
481 void TimeoutManager::Initialize() {
482 Preferences::AddIntVarCache(&gMinClampTimeoutValue, "dom.min_timeout_value",
483 DEFAULT_MIN_CLAMP_TIMEOUT_VALUE);
484 Preferences::AddIntVarCache(&gMinBackgroundTimeoutValue,
485 "dom.min_background_timeout_value",
486 DEFAULT_MIN_BACKGROUND_TIMEOUT_VALUE);
487 Preferences::AddIntVarCache(&gMinTrackingTimeoutValue,
488 "dom.min_tracking_timeout_value",
489 DEFAULT_MIN_TRACKING_TIMEOUT_VALUE);
490 Preferences::AddIntVarCache(&gMinTrackingBackgroundTimeoutValue,
491 "dom.min_tracking_background_timeout_value",
492 DEFAULT_MIN_TRACKING_BACKGROUND_TIMEOUT_VALUE);
493 Preferences::AddIntVarCache(&gTimeoutThrottlingDelay,
494 "dom.timeout.throttling_delay",
495 DEFAULT_TIMEOUT_THROTTLING_DELAY);
497 Preferences::AddUintVarCache(&gMaxConsecutiveCallbacksMilliseconds,
498 "dom.timeout.max_consecutive_callbacks_ms",
499 DEFAULT_MAX_CONSECUTIVE_CALLBACKS_MILLISECONDS);
501 Preferences::AddIntVarCache(&gDisableOpenClickDelay,
502 "dom.disable_open_click_delay",
503 DEFAULT_DISABLE_OPEN_CLICK_DELAY);
504 Preferences::AddIntVarCache(&gBackgroundBudgetRegenerationFactor,
505 "dom.timeout.background_budget_regeneration_rate",
506 DEFAULT_BACKGROUND_BUDGET_REGENERATION_FACTOR);
507 Preferences::AddIntVarCache(&gForegroundBudgetRegenerationFactor,
508 "dom.timeout.foreground_budget_regeneration_rate",
509 DEFAULT_FOREGROUND_BUDGET_REGENERATION_FACTOR);
510 Preferences::AddIntVarCache(&gBackgroundThrottlingMaxBudget,
511 "dom.timeout.background_throttling_max_budget",
512 DEFAULT_BACKGROUND_THROTTLING_MAX_BUDGET);
513 Preferences::AddIntVarCache(&gForegroundThrottlingMaxBudget,
514 "dom.timeout.foreground_throttling_max_budget",
515 DEFAULT_FOREGROUND_THROTTLING_MAX_BUDGET);
516 Preferences::AddIntVarCache(&gBudgetThrottlingMaxDelay,
517 "dom.timeout.budget_throttling_max_delay",
518 DEFAULT_BUDGET_THROTTLING_MAX_DELAY);
519 Preferences::AddBoolVarCache(&gEnableBudgetTimeoutThrottling,
520 "dom.timeout.enable_budget_timer_throttling",
521 DEFAULT_ENABLE_BUDGET_TIMEOUT_THROTTLING);
524 uint32_t TimeoutManager::GetTimeoutId(Timeout::Reason aReason) {
525 switch (aReason) {
526 case Timeout::Reason::eIdleCallbackTimeout:
527 return ++mIdleCallbackTimeoutCounter;
528 case Timeout::Reason::eTimeoutOrInterval:
529 default:
530 return ++mTimeoutIdCounter;
534 bool TimeoutManager::IsRunningTimeout() const { return mRunningTimeout; }
536 nsresult TimeoutManager::SetTimeout(nsITimeoutHandler* aHandler,
537 int32_t interval, bool aIsInterval,
538 Timeout::Reason aReason, int32_t* aReturn) {
539 // If we don't have a document (we could have been unloaded since
540 // the call to setTimeout was made), do nothing.
541 nsCOMPtr<Document> doc = mWindow.GetExtantDoc();
542 if (!doc) {
543 return NS_OK;
546 // Disallow negative intervals.
547 interval = std::max(0, interval);
549 // Make sure we don't proceed with an interval larger than our timer
550 // code can handle. (Note: we already forced |interval| to be non-negative,
551 // so the uint32_t cast (to avoid compiler warnings) is ok.)
552 uint32_t maxTimeoutMs = PR_IntervalToMilliseconds(DOM_MAX_TIMEOUT_VALUE);
553 if (static_cast<uint32_t>(interval) > maxTimeoutMs) {
554 interval = maxTimeoutMs;
557 RefPtr<Timeout> timeout = new Timeout();
558 #ifdef DEBUG
559 timeout->mFiringIndex = -1;
560 #endif
561 timeout->mWindow = &mWindow;
562 timeout->mIsInterval = aIsInterval;
563 timeout->mInterval = TimeDuration::FromMilliseconds(interval);
564 timeout->mScriptHandler = aHandler;
565 timeout->mReason = aReason;
567 // No popups from timeouts by default
568 timeout->mPopupState = PopupBlocker::openAbused;
570 timeout->mNestingLevel = sNestingLevel < DOM_CLAMP_TIMEOUT_NESTING_LEVEL
571 ? sNestingLevel + 1
572 : sNestingLevel;
574 // Now clamp the actual interval we will use for the timer based on
575 TimeDuration realInterval = CalculateDelay(timeout);
576 TimeStamp now = TimeStamp::Now();
577 timeout->SetWhenOrTimeRemaining(now, realInterval);
579 // If we're not suspended, then set the timer.
580 if (!mWindow.IsSuspended()) {
581 nsresult rv = MaybeSchedule(timeout->When(), now);
582 if (NS_FAILED(rv)) {
583 return rv;
587 if (gRunningTimeoutDepth == 0 &&
588 PopupBlocker::GetPopupControlState() < PopupBlocker::openBlocked) {
589 // This timeout is *not* set from another timeout and it's set
590 // while popups are enabled. Propagate the state to the timeout if
591 // its delay (interval) is equal to or less than what
592 // "dom.disable_open_click_delay" is set to (in ms).
594 // This is checking |interval|, not realInterval, on purpose,
595 // because our lower bound for |realInterval| could be pretty high
596 // in some cases.
597 if (interval <= gDisableOpenClickDelay) {
598 timeout->mPopupState = PopupBlocker::GetPopupControlState();
602 Timeouts::SortBy sort(mWindow.IsFrozen() ? Timeouts::SortBy::TimeRemaining
603 : Timeouts::SortBy::TimeWhen);
604 mTimeouts.Insert(timeout, sort);
606 timeout->mTimeoutId = GetTimeoutId(aReason);
607 *aReturn = timeout->mTimeoutId;
609 MOZ_LOG(
610 gTimeoutLog, LogLevel::Debug,
611 ("Set%s(TimeoutManager=%p, timeout=%p, delay=%i, "
612 "minimum=%f, throttling=%s, state=%s(%s), realInterval=%f) "
613 "returned timeout ID %u, budget=%d\n",
614 aIsInterval ? "Interval" : "Timeout", this, timeout.get(), interval,
615 (CalculateDelay(timeout) - timeout->mInterval).ToMilliseconds(),
616 mThrottleTimeouts ? "yes" : (mThrottleTimeoutsTimer ? "pending" : "no"),
617 IsActive() ? "active" : "inactive",
618 mWindow.IsBackgroundInternal() ? "background" : "foreground",
619 realInterval.ToMilliseconds(), timeout->mTimeoutId,
620 int(mExecutionBudget.ToMilliseconds())));
622 return NS_OK;
625 // Make sure we clear it no matter which list it's in
626 void TimeoutManager::ClearTimeout(int32_t aTimerId, Timeout::Reason aReason) {
627 if (ClearTimeoutInternal(aTimerId, aReason, false) ||
628 mIdleTimeouts.IsEmpty()) {
629 return; // no need to check the other list if we cleared the timeout
631 ClearTimeoutInternal(aTimerId, aReason, true);
634 bool TimeoutManager::ClearTimeoutInternal(int32_t aTimerId,
635 Timeout::Reason aReason,
636 bool aIsIdle) {
637 uint32_t timerId = (uint32_t)aTimerId;
638 Timeouts& timeouts = aIsIdle ? mIdleTimeouts : mTimeouts;
639 RefPtr<TimeoutExecutor>& executor = aIsIdle ? mIdleExecutor : mExecutor;
640 bool firstTimeout = true;
641 bool deferredDeletion = false;
642 bool cleared = false;
644 timeouts.ForEachAbortable([&](Timeout* aTimeout) {
645 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
646 ("Clear%s(TimeoutManager=%p, timeout=%p, aTimerId=%u, ID=%u)\n",
647 aTimeout->mIsInterval ? "Interval" : "Timeout", this, aTimeout,
648 timerId, aTimeout->mTimeoutId));
650 if (aTimeout->mTimeoutId == timerId && aTimeout->mReason == aReason) {
651 if (aTimeout->mRunning) {
652 /* We're running from inside the aTimeout. Mark this
653 aTimeout for deferred deletion by the code in
654 RunTimeout() */
655 aTimeout->mIsInterval = false;
656 deferredDeletion = true;
657 } else {
658 /* Delete the aTimeout from the pending aTimeout list */
659 aTimeout->remove();
661 cleared = true;
662 return true; // abort!
665 firstTimeout = false;
667 return false;
670 // We don't need to reschedule the executor if any of the following are true:
671 // * If the we weren't cancelling the first timeout, then the executor's
672 // state doesn't need to change. It will only reflect the next soonest
673 // Timeout.
674 // * If we did cancel the first Timeout, but its currently running, then
675 // RunTimeout() will handle rescheduling the executor.
676 // * If the window has become suspended then we should not start executing
677 // Timeouts.
678 if (!firstTimeout || deferredDeletion || mWindow.IsSuspended()) {
679 return cleared;
682 // Stop the executor and restart it at the next soonest deadline.
683 executor->Cancel();
685 Timeout* nextTimeout = timeouts.GetFirst();
686 if (nextTimeout) {
687 if (aIsIdle) {
688 MOZ_ALWAYS_SUCCEEDS(
689 executor->MaybeSchedule(nextTimeout->When(), TimeDuration(0)));
690 } else {
691 MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextTimeout->When()));
694 return cleared;
697 void TimeoutManager::RunTimeout(const TimeStamp& aNow,
698 const TimeStamp& aTargetDeadline,
699 bool aProcessIdle) {
700 MOZ_DIAGNOSTIC_ASSERT(!aNow.IsNull());
701 MOZ_DIAGNOSTIC_ASSERT(!aTargetDeadline.IsNull());
703 MOZ_ASSERT_IF(mWindow.IsFrozen(), mWindow.IsSuspended());
704 if (mWindow.IsSuspended()) {
705 return;
708 Timeouts& timeouts(aProcessIdle ? mIdleTimeouts : mTimeouts);
710 // Limit the overall time spent in RunTimeout() to reduce jank.
711 uint32_t totalTimeLimitMS =
712 std::max(1u, gMaxConsecutiveCallbacksMilliseconds);
713 const TimeDuration totalTimeLimit =
714 TimeDuration::Min(TimeDuration::FromMilliseconds(totalTimeLimitMS),
715 TimeDuration::Max(TimeDuration(), mExecutionBudget));
717 // Allow up to 25% of our total time budget to be used figuring out which
718 // timers need to run. This is the initial loop in this method.
719 const TimeDuration initialTimeLimit =
720 TimeDuration::FromMilliseconds(totalTimeLimit.ToMilliseconds() / 4);
722 // Ammortize overhead from from calling TimeStamp::Now() in the initial
723 // loop, though, by only checking for an elapsed limit every N timeouts.
724 const uint32_t kNumTimersPerInitialElapsedCheck = 100;
726 // Start measuring elapsed time immediately. We won't potentially expire
727 // the time budget until at least one Timeout has run, though.
728 TimeStamp now(aNow);
729 TimeStamp start = now;
731 uint32_t firingId = CreateFiringId();
732 auto guard = MakeScopeExit([&] { DestroyFiringId(firingId); });
734 // Make sure that the window and the script context don't go away as
735 // a result of running timeouts
736 RefPtr<nsGlobalWindowInner> window(&mWindow);
737 // Accessing members of mWindow here is safe, because the lifetime of
738 // TimeoutManager is the same as the lifetime of the containing
739 // nsGlobalWindow.
741 // A native timer has gone off. See which of our timeouts need
742 // servicing
743 TimeStamp deadline;
745 if (aTargetDeadline > now) {
746 // The OS timer fired early (which can happen due to the timers
747 // having lower precision than TimeStamp does). Set |deadline| to
748 // be the time when the OS timer *should* have fired so that any
749 // timers that *should* have fired *will* be fired now.
751 deadline = aTargetDeadline;
752 } else {
753 deadline = now;
756 TimeStamp nextDeadline;
757 uint32_t numTimersToRun = 0;
759 // The timeout list is kept in deadline order. Discover the latest timeout
760 // whose deadline has expired. On some platforms, native timeout events fire
761 // "early", but we handled that above by setting deadline to aTargetDeadline
762 // if the timer fired early. So we can stop walking if we get to timeouts
763 // whose When() is greater than deadline, since once that happens we know
764 // nothing past that point is expired.
766 for (Timeout* timeout = timeouts.GetFirst(); timeout != nullptr;
767 timeout = timeout->getNext()) {
768 if (totalTimeLimit.IsZero() || timeout->When() > deadline) {
769 nextDeadline = timeout->When();
770 break;
773 if (IsInvalidFiringId(timeout->mFiringId)) {
774 // Mark any timeouts that are on the list to be fired with the
775 // firing depth so that we can reentrantly run timeouts
776 timeout->mFiringId = firingId;
778 numTimersToRun += 1;
780 // Run only a limited number of timers based on the configured maximum.
781 if (numTimersToRun % kNumTimersPerInitialElapsedCheck == 0) {
782 now = TimeStamp::Now();
783 TimeDuration elapsed(now - start);
784 if (elapsed >= initialTimeLimit) {
785 nextDeadline = timeout->When();
786 break;
791 if (aProcessIdle) {
792 MOZ_LOG(
793 gTimeoutLog, LogLevel::Debug,
794 ("Running %u deferred timeouts on idle (TimeoutManager=%p), "
795 "nextDeadline = %gms from now",
796 numTimersToRun, this,
797 nextDeadline.IsNull() ? 0.0 : (nextDeadline - now).ToMilliseconds()));
800 now = TimeStamp::Now();
802 // Wherever we stopped in the timer list, schedule the executor to
803 // run for the next unexpired deadline. Note, this *must* be done
804 // before we start executing any content script handlers. If one
805 // of them spins the event loop the executor must already be scheduled
806 // in order for timeouts to fire properly.
807 if (!nextDeadline.IsNull()) {
808 // Note, we verified the window is not suspended at the top of
809 // method and the window should not have been suspended while
810 // executing the loop above since it doesn't call out to js.
811 MOZ_DIAGNOSTIC_ASSERT(!mWindow.IsSuspended());
812 if (aProcessIdle) {
813 // We don't want to update timing budget for idle queue firings, and
814 // all timeouts in the IdleTimeouts list have hit their deadlines,
815 // and so should run as soon as possible.
816 MOZ_ALWAYS_SUCCEEDS(
817 mIdleExecutor->MaybeSchedule(nextDeadline, TimeDuration()));
818 } else {
819 MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextDeadline, now));
823 // Maybe the timeout that the event was fired for has been deleted
824 // and there are no others timeouts with deadlines that make them
825 // eligible for execution yet. Go away.
826 if (!numTimersToRun) {
827 return;
830 // Now we need to search the normal and tracking timer list at the same
831 // time to run the timers in the scheduled order.
833 // We stop iterating each list when we go past the last expired timeout from
834 // that list that we have observed above. That timeout will either be the
835 // next item after the last timeout we looked at or nullptr if we have
836 // exhausted the entire list while looking for the last expired timeout.
838 // Use a nested scope in order to make sure the strong references held while
839 // iterating are freed after the loop.
841 // The next timeout to run. This is used to advance the loop, but
842 // we cannot set it until we've run the current timeout, since
843 // running the current timeout might remove the immediate next
844 // timeout.
845 RefPtr<Timeout> next;
847 for (RefPtr<Timeout> timeout = timeouts.GetFirst(); timeout != nullptr;
848 timeout = next) {
849 next = timeout->getNext();
850 // We should only execute callbacks for the set of expired Timeout
851 // objects we computed above.
852 if (timeout->mFiringId != firingId) {
853 // If the FiringId does not match, but is still valid, then this is
854 // a Timeout for another RunTimeout() on the call stack (such as in
855 // the case of nested event loops, for alert() or more likely XHR).
856 // Just skip it.
857 if (IsValidFiringId(timeout->mFiringId)) {
858 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
859 ("Skipping Run%s(TimeoutManager=%p, timeout=%p) since "
860 "firingId %d is valid (processing firingId %d)"
861 #ifdef DEBUG
862 " - FiringIndex %" PRId64 " (mLastFiringIndex %" PRId64 ")"
863 #endif
865 timeout->mIsInterval ? "Interval" : "Timeout", this,
866 timeout.get(), timeout->mFiringId, firingId
867 #ifdef DEBUG
869 timeout->mFiringIndex, mFiringIndex
870 #endif
872 #ifdef DEBUG
873 // The old FiringIndex assumed no recursion; recursion can cause
874 // other timers to get fired "in the middle" of a sequence we've
875 // already assigned firingindexes to. Since we're not going to
876 // run this timeout now, remove any FiringIndex that was already
877 // set.
879 // Since all timers that have FiringIndexes set *must* be ready
880 // to run and have valid FiringIds, all of them will be 'skipped'
881 // and reset if we recurse - we don't have to look through the
882 // list past where we'll stop on the first InvalidFiringId.
883 timeout->mFiringIndex = -1;
884 #endif
885 continue;
888 // If, however, the FiringId is invalid then we have reached Timeout
889 // objects beyond the list we calculated above. This can happen
890 // if the Timeout just beyond our last expired Timeout is cancelled
891 // by one of the callbacks we've just executed. In this case we
892 // should just stop iterating. We're done.
893 else {
894 break;
898 MOZ_ASSERT_IF(mWindow.IsFrozen(), mWindow.IsSuspended());
899 if (mWindow.IsSuspended()) {
900 break;
903 // The timeout is on the list to run at this depth, go ahead and
904 // process it.
906 // Record the first time we try to fire a timeout, and ensure that
907 // all actual firings occur in that order. This ensures that we
908 // retain compliance with the spec language
909 // (https://html.spec.whatwg.org/#dom-settimeout) specifically items
910 // 15 ("If method context is a Window object, wait until the Document
911 // associated with method context has been fully active for a further
912 // timeout milliseconds (not necessarily consecutively)") and item 16
913 // ("Wait until any invocations of this algorithm that had the same
914 // method context, that started before this one, and whose timeout is
915 // equal to or less than this one's, have completed.").
916 #ifdef DEBUG
917 if (timeout->mFiringIndex == -1) {
918 timeout->mFiringIndex = mFiringIndex++;
920 #endif
922 if (mIsLoading && !aProcessIdle) {
923 // Any timeouts that would fire during a load will be deferred
924 // until the load event occurs, but if there's an idle time,
925 // they'll be run before the load event.
926 timeout->remove();
927 // MOZ_RELEASE_ASSERT(timeout->When() <= (TimeStamp::Now()));
928 mIdleTimeouts.InsertBack(timeout);
929 if (MOZ_LOG_TEST(gTimeoutLog, LogLevel::Debug)) {
930 uint32_t num = 0;
931 for (Timeout* t = mIdleTimeouts.GetFirst(); t != nullptr;
932 t = t->getNext()) {
933 num++;
935 MOZ_LOG(
936 gTimeoutLog, LogLevel::Debug,
937 ("Deferring Run%s(TimeoutManager=%p, timeout=%p (%gms in the "
938 "past)) (%u deferred)",
939 timeout->mIsInterval ? "Interval" : "Timeout", this,
940 timeout.get(), (now - timeout->When()).ToMilliseconds(), num));
942 MOZ_ALWAYS_SUCCEEDS(mIdleExecutor->MaybeSchedule(now, TimeDuration()));
943 } else {
944 // Get the script context (a strong ref to prevent it going away)
945 // for this timeout and ensure the script language is enabled.
946 nsCOMPtr<nsIScriptContext> scx = mWindow.GetContextInternal();
948 if (!scx) {
949 // No context means this window was closed or never properly
950 // initialized for this language. This timer will never fire
951 // so just remove it.
952 timeout->remove();
953 continue;
956 #ifdef DEBUG
957 if (timeout->mFiringIndex <= mLastFiringIndex) {
958 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
959 ("Incorrect firing index for Run%s(TimeoutManager=%p, "
960 "timeout=%p) with "
961 "firingId %d - FiringIndex %" PRId64
962 " (mLastFiringIndex %" PRId64 ")",
963 timeout->mIsInterval ? "Interval" : "Timeout", this,
964 timeout.get(), timeout->mFiringId, timeout->mFiringIndex,
965 mFiringIndex));
967 MOZ_ASSERT(timeout->mFiringIndex > mLastFiringIndex);
968 mLastFiringIndex = timeout->mFiringIndex;
969 #endif
970 // This timeout is good to run.
971 bool timeout_was_cleared = window->RunTimeoutHandler(timeout, scx);
972 #if MOZ_GECKO_PROFILER
973 if (profiler_is_active()) {
974 TimeDuration elapsed = now - timeout->SubmitTime();
975 TimeDuration target = timeout->When() - timeout->SubmitTime();
976 TimeDuration delta = now - timeout->When();
977 TimeDuration runtime = TimeStamp::Now() - now;
978 nsPrintfCString marker(
979 "%sset%s() for %dms (original target time was %dms (%dms "
980 "delta)); runtime = %dms",
981 aProcessIdle ? "Deferred " : "",
982 timeout->mIsInterval ? "Interval" : "Timeout",
983 int(elapsed.ToMilliseconds()), int(target.ToMilliseconds()),
984 int(delta.ToMilliseconds()), int(runtime.ToMilliseconds()));
985 // don't have end before start...
986 profiler_add_marker(
987 "setTimeout", JS::ProfilingCategoryPair::DOM,
988 MakeUnique<TextMarkerPayload>(
989 marker, delta.ToMilliseconds() >= 0 ? timeout->When() : now,
990 now));
992 #endif
994 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
995 ("Run%s(TimeoutManager=%p, timeout=%p) returned %d\n",
996 timeout->mIsInterval ? "Interval" : "Timeout", this,
997 timeout.get(), !!timeout_was_cleared));
999 if (timeout_was_cleared) {
1000 // Make sure we're not holding any Timeout objects alive.
1001 next = nullptr;
1003 // Since ClearAllTimeouts() was called the lists should be empty.
1004 MOZ_DIAGNOSTIC_ASSERT(!HasTimeouts());
1006 return;
1009 // If we need to reschedule a setInterval() the delay should be
1010 // calculated based on when its callback started to execute. So
1011 // save off the last time before updating our "now" timestamp to
1012 // account for its callback execution time.
1013 TimeStamp lastCallbackTime = now;
1014 now = TimeStamp::Now();
1016 // If we have a regular interval timer, we re-schedule the
1017 // timeout, accounting for clock drift.
1018 bool needsReinsertion =
1019 RescheduleTimeout(timeout, lastCallbackTime, now);
1021 // Running a timeout can cause another timeout to be deleted, so
1022 // we need to reset the pointer to the following timeout.
1023 next = timeout->getNext();
1025 timeout->remove();
1027 if (needsReinsertion) {
1028 // Insert interval timeout onto the corresponding list sorted in
1029 // deadline order. AddRefs timeout.
1030 // Always re-insert into the normal time queue!
1031 mTimeouts.Insert(timeout, mWindow.IsFrozen()
1032 ? Timeouts::SortBy::TimeRemaining
1033 : Timeouts::SortBy::TimeWhen);
1036 // Check to see if we have run out of time to execute timeout handlers.
1037 // If we've exceeded our time budget then terminate the loop immediately.
1038 TimeDuration elapsed = now - start;
1039 if (elapsed >= totalTimeLimit) {
1040 // We ran out of time. Make sure to schedule the executor to
1041 // run immediately for the next timer, if it exists. Its possible,
1042 // however, that the last timeout handler suspended the window. If
1043 // that happened then we must skip this step.
1044 if (!mWindow.IsSuspended()) {
1045 if (next) {
1046 if (aProcessIdle) {
1047 // We don't want to update timing budget for idle queue firings,
1048 // and all timeouts in the IdleTimeouts list have hit their
1049 // deadlines, and so should run as soon as possible.
1051 // Shouldn't need cancelling since it never waits
1052 MOZ_ALWAYS_SUCCEEDS(
1053 mIdleExecutor->MaybeSchedule(next->When(), TimeDuration()));
1054 } else {
1055 // If we ran out of execution budget we need to force a
1056 // reschedule. By cancelling the executor we will not run
1057 // immediately, but instead reschedule to the minimum
1058 // scheduling delay.
1059 if (mExecutionBudget < TimeDuration()) {
1060 mExecutor->Cancel();
1063 MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(next->When(), now));
1067 break;
1073 bool TimeoutManager::RescheduleTimeout(Timeout* aTimeout,
1074 const TimeStamp& aLastCallbackTime,
1075 const TimeStamp& aCurrentNow) {
1076 MOZ_DIAGNOSTIC_ASSERT(aLastCallbackTime <= aCurrentNow);
1078 if (!aTimeout->mIsInterval) {
1079 return false;
1082 // Automatically increase the nesting level when a setInterval()
1083 // is rescheduled just as if it was using a chained setTimeout().
1084 if (aTimeout->mNestingLevel < DOM_CLAMP_TIMEOUT_NESTING_LEVEL) {
1085 aTimeout->mNestingLevel += 1;
1088 // Compute time to next timeout for interval timer.
1089 // Make sure nextInterval is at least CalculateDelay().
1090 TimeDuration nextInterval = CalculateDelay(aTimeout);
1092 TimeStamp firingTime = aLastCallbackTime + nextInterval;
1093 TimeDuration delay = firingTime - aCurrentNow;
1095 #ifdef DEBUG
1096 aTimeout->mFiringIndex = -1;
1097 #endif
1098 // And make sure delay is nonnegative; that might happen if the timer
1099 // thread is firing our timers somewhat early or if they're taking a long
1100 // time to run the callback.
1101 if (delay < TimeDuration(0)) {
1102 delay = TimeDuration(0);
1105 aTimeout->SetWhenOrTimeRemaining(aCurrentNow, delay);
1107 if (mWindow.IsSuspended()) {
1108 return true;
1111 nsresult rv = MaybeSchedule(aTimeout->When(), aCurrentNow);
1112 NS_ENSURE_SUCCESS(rv, false);
1114 return true;
1117 void TimeoutManager::ClearAllTimeouts() {
1118 bool seenRunningTimeout = false;
1120 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
1121 ("ClearAllTimeouts(TimeoutManager=%p)\n", this));
1123 if (mThrottleTimeoutsTimer) {
1124 mThrottleTimeoutsTimer->Cancel();
1125 mThrottleTimeoutsTimer = nullptr;
1128 mExecutor->Cancel();
1129 mIdleExecutor->Cancel();
1131 ForEachUnorderedTimeout([&](Timeout* aTimeout) {
1132 /* If RunTimeout() is higher up on the stack for this
1133 window, e.g. as a result of document.write from a timeout,
1134 then we need to reset the list insertion point for
1135 newly-created timeouts in case the user adds a timeout,
1136 before we pop the stack back to RunTimeout. */
1137 if (mRunningTimeout == aTimeout) {
1138 seenRunningTimeout = true;
1141 // Set timeout->mCleared to true to indicate that the timeout was
1142 // cleared and taken out of the list of timeouts
1143 aTimeout->mCleared = true;
1146 // Clear out our lists
1147 mTimeouts.Clear();
1148 mIdleTimeouts.Clear();
1151 void TimeoutManager::Timeouts::Insert(Timeout* aTimeout, SortBy aSortBy) {
1152 // Start at mLastTimeout and go backwards. Stop if we see a Timeout with a
1153 // valid FiringId since those timers are currently being processed by
1154 // RunTimeout. This optimizes for the common case of insertion at the end.
1155 Timeout* prevSibling;
1156 for (prevSibling = GetLast();
1157 prevSibling &&
1158 // This condition needs to match the one in SetTimeoutOrInterval that
1159 // determines whether to set When() or TimeRemaining().
1160 (aSortBy == SortBy::TimeRemaining
1161 ? prevSibling->TimeRemaining() > aTimeout->TimeRemaining()
1162 : prevSibling->When() > aTimeout->When()) &&
1163 // Check the firing ID last since it will evaluate true in the vast
1164 // majority of cases.
1165 mManager.IsInvalidFiringId(prevSibling->mFiringId);
1166 prevSibling = prevSibling->getPrevious()) {
1167 /* Do nothing; just searching */
1170 // Now link in aTimeout after prevSibling.
1171 if (prevSibling) {
1172 prevSibling->setNext(aTimeout);
1173 } else {
1174 InsertFront(aTimeout);
1177 aTimeout->mFiringId = InvalidFiringId;
1180 Timeout* TimeoutManager::BeginRunningTimeout(Timeout* aTimeout) {
1181 Timeout* currentTimeout = mRunningTimeout;
1182 mRunningTimeout = aTimeout;
1183 ++gRunningTimeoutDepth;
1185 RecordExecution(currentTimeout, aTimeout);
1186 return currentTimeout;
1189 void TimeoutManager::EndRunningTimeout(Timeout* aTimeout) {
1190 --gRunningTimeoutDepth;
1192 RecordExecution(mRunningTimeout, aTimeout);
1193 mRunningTimeout = aTimeout;
1196 void TimeoutManager::UnmarkGrayTimers() {
1197 ForEachUnorderedTimeout([](Timeout* aTimeout) {
1198 if (aTimeout->mScriptHandler) {
1199 aTimeout->mScriptHandler->MarkForCC();
1204 void TimeoutManager::Suspend() {
1205 MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Suspend(TimeoutManager=%p)\n", this));
1207 if (mThrottleTimeoutsTimer) {
1208 mThrottleTimeoutsTimer->Cancel();
1209 mThrottleTimeoutsTimer = nullptr;
1212 mExecutor->Cancel();
1213 mIdleExecutor->Cancel();
1216 void TimeoutManager::Resume() {
1217 MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Resume(TimeoutManager=%p)\n", this));
1219 // When Suspend() has been called after IsDocumentLoaded(), but the
1220 // throttle tracking timer never managed to fire, start the timer
1221 // again.
1222 if (mWindow.IsDocumentLoaded() && !mThrottleTimeouts) {
1223 MaybeStartThrottleTimeout();
1226 Timeout* nextTimeout = mTimeouts.GetFirst();
1227 if (nextTimeout) {
1228 MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextTimeout->When()));
1230 nextTimeout = mIdleTimeouts.GetFirst();
1231 if (nextTimeout) {
1232 MOZ_ALWAYS_SUCCEEDS(
1233 mIdleExecutor->MaybeSchedule(nextTimeout->When(), TimeDuration()));
1237 void TimeoutManager::Freeze() {
1238 MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Freeze(TimeoutManager=%p)\n", this));
1240 TimeStamp now = TimeStamp::Now();
1241 ForEachUnorderedTimeout([&](Timeout* aTimeout) {
1242 // Save the current remaining time for this timeout. We will
1243 // re-apply it when the window is Thaw()'d. This effectively
1244 // shifts timers to the right as if time does not pass while
1245 // the window is frozen.
1246 TimeDuration delta(0);
1247 if (aTimeout->When() > now) {
1248 delta = aTimeout->When() - now;
1250 aTimeout->SetWhenOrTimeRemaining(now, delta);
1251 MOZ_DIAGNOSTIC_ASSERT(aTimeout->TimeRemaining() == delta);
1255 void TimeoutManager::Thaw() {
1256 MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Thaw(TimeoutManager=%p)\n", this));
1258 TimeStamp now = TimeStamp::Now();
1260 ForEachUnorderedTimeout([&](Timeout* aTimeout) {
1261 // Set When() back to the time when the timer is supposed to fire.
1262 aTimeout->SetWhenOrTimeRemaining(now, aTimeout->TimeRemaining());
1263 MOZ_DIAGNOSTIC_ASSERT(!aTimeout->When().IsNull());
1267 void TimeoutManager::UpdateBackgroundState() {
1268 mExecutionBudget = GetMaxBudget(mWindow.IsBackgroundInternal());
1270 // When the window moves to the background or foreground we should
1271 // reschedule the TimeoutExecutor in case the MinSchedulingDelay()
1272 // changed. Only do this if the window is not suspended and we
1273 // actually have a timeout.
1274 if (!mWindow.IsSuspended()) {
1275 Timeout* nextTimeout = mTimeouts.GetFirst();
1276 if (nextTimeout) {
1277 mExecutor->Cancel();
1278 MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextTimeout->When()));
1280 // the Idle queue should all be past their firing time, so there we just
1281 // need to restart the queue
1283 // XXX May not be needed if we don't stop the idle queue, as
1284 // MinSchedulingDelay isn't relevant here
1285 nextTimeout = mIdleTimeouts.GetFirst();
1286 if (nextTimeout) {
1287 mIdleExecutor->Cancel();
1288 MOZ_ALWAYS_SUCCEEDS(
1289 mIdleExecutor->MaybeSchedule(nextTimeout->When(), TimeDuration()));
1294 namespace {
1296 class ThrottleTimeoutsCallback final : public nsITimerCallback,
1297 public nsINamed {
1298 public:
1299 explicit ThrottleTimeoutsCallback(nsGlobalWindowInner* aWindow)
1300 : mWindow(aWindow) {}
1302 NS_DECL_ISUPPORTS
1303 NS_DECL_NSITIMERCALLBACK
1305 NS_IMETHOD GetName(nsACString& aName) override {
1306 aName.AssignLiteral("ThrottleTimeoutsCallback");
1307 return NS_OK;
1310 private:
1311 ~ThrottleTimeoutsCallback() {}
1313 private:
1314 // The strong reference here keeps the Window and hence the TimeoutManager
1315 // object itself alive.
1316 RefPtr<nsGlobalWindowInner> mWindow;
1319 NS_IMPL_ISUPPORTS(ThrottleTimeoutsCallback, nsITimerCallback, nsINamed)
1321 NS_IMETHODIMP
1322 ThrottleTimeoutsCallback::Notify(nsITimer* aTimer) {
1323 mWindow->TimeoutManager().StartThrottlingTimeouts();
1324 mWindow = nullptr;
1325 return NS_OK;
1328 } // namespace
1330 bool TimeoutManager::BudgetThrottlingEnabled(bool aIsBackground) const {
1331 // A window can be throttled using budget if
1332 // * It isn't active
1333 // * If it isn't using WebRTC
1334 // * If it hasn't got open WebSockets
1335 // * If it hasn't got active IndexedDB databases
1337 // Note that we allow both foreground and background to be
1338 // considered for budget throttling. What determines if they are if
1339 // budget throttling is enabled is the max budget.
1340 if ((aIsBackground ? gBackgroundThrottlingMaxBudget
1341 : gForegroundThrottlingMaxBudget) < 0) {
1342 return false;
1345 if (!mBudgetThrottleTimeouts || IsActive()) {
1346 return false;
1349 // Check if there are any active IndexedDB databases
1350 if (mWindow.HasActiveIndexedDBDatabases()) {
1351 return false;
1354 // Check if we have active PeerConnection
1355 if (mWindow.HasActivePeerConnections()) {
1356 return false;
1359 if (mWindow.HasOpenWebSockets()) {
1360 return false;
1363 return true;
1366 void TimeoutManager::StartThrottlingTimeouts() {
1367 MOZ_ASSERT(NS_IsMainThread());
1368 MOZ_DIAGNOSTIC_ASSERT(mThrottleTimeoutsTimer);
1370 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
1371 ("TimeoutManager %p started to throttle tracking timeouts\n", this));
1373 MOZ_DIAGNOSTIC_ASSERT(!mThrottleTimeouts);
1374 mThrottleTimeouts = true;
1375 mThrottleTrackingTimeouts = true;
1376 mBudgetThrottleTimeouts = gEnableBudgetTimeoutThrottling;
1377 mThrottleTimeoutsTimer = nullptr;
1380 void TimeoutManager::OnDocumentLoaded() {
1381 // The load event may be firing again if we're coming back to the page by
1382 // navigating through the session history, so we need to ensure to only call
1383 // this when mThrottleTimeouts hasn't been set yet.
1384 if (!mThrottleTimeouts) {
1385 MaybeStartThrottleTimeout();
1389 void TimeoutManager::MaybeStartThrottleTimeout() {
1390 if (gTimeoutThrottlingDelay <= 0 || mWindow.IsDying() ||
1391 mWindow.IsSuspended()) {
1392 return;
1395 MOZ_DIAGNOSTIC_ASSERT(!mThrottleTimeouts);
1397 MOZ_LOG(gTimeoutLog, LogLevel::Debug,
1398 ("TimeoutManager %p delaying tracking timeout throttling by %dms\n",
1399 this, gTimeoutThrottlingDelay));
1401 nsCOMPtr<nsITimerCallback> callback = new ThrottleTimeoutsCallback(&mWindow);
1403 NS_NewTimerWithCallback(getter_AddRefs(mThrottleTimeoutsTimer), callback,
1404 gTimeoutThrottlingDelay, nsITimer::TYPE_ONE_SHOT,
1405 EventTarget());
1408 void TimeoutManager::BeginSyncOperation() {
1409 // If we're beginning a sync operation, the currently running
1410 // timeout will be put on hold. To not get into an inconsistent
1411 // state, where the currently running timeout appears to take time
1412 // equivalent to the period of us spinning up a new event loop,
1413 // record what we have and stop recording until we reach
1414 // EndSyncOperation.
1415 RecordExecution(mRunningTimeout, nullptr);
1418 void TimeoutManager::EndSyncOperation() {
1419 // If we're running a timeout, restart the measurement from here.
1420 RecordExecution(nullptr, mRunningTimeout);
1423 nsIEventTarget* TimeoutManager::EventTarget() {
1424 return mWindow.EventTargetFor(TaskCategory::Timer);