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