Coverage Report

Created: 2018-09-25 14:53

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