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