Source code

Revision control

Other Tools

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