LCOV - code coverage report
Current view: top level - dom/base - TimeoutManager.cpp (source / functions) Hit Total Coverage
Test: output.info Lines: 274 456 60.1 %
Date: 2017-07-14 16:53:18 Functions: 31 52 59.6 %
Legend: Lines: hit not hit

          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             : }

Generated by: LCOV version 1.13