Bug 1363829 P6 Use the TimeoutExecutor in TimeoutManager. r=ehsan
authorBen Kelly <ben@wanderview.com>
Wed, 31 May 2017 17:13:19 -0700
changeset 361742 5214ce6bed250add6892a0c7f2eeca6807f71cef
parent 361741 9ac282f673708b4ec618b135829da348529778f4
child 361743 32c98a6c875072be8d8dc38ec3f07512e66fc9e2
push id31940
push usercbook@mozilla.com
push dateThu, 01 Jun 2017 11:51:11 +0000
treeherdermozilla-central@0bcea6bac179 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan
bugs1363829
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1363829 P6 Use the TimeoutExecutor in TimeoutManager. r=ehsan
dom/base/Timeout.cpp
dom/base/TimeoutExecutor.cpp
dom/base/TimeoutManager.cpp
dom/base/TimeoutManager.h
--- a/dom/base/Timeout.cpp
+++ b/dom/base/Timeout.cpp
@@ -53,17 +53,17 @@ NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(Tim
 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(Timeout, Release)
 
 namespace {
 
 void
 TimerCallback(nsITimer*, void* aClosure)
 {
   RefPtr<Timeout> timeout = (Timeout*)aClosure;
-  timeout->mWindow->AsInner()->TimeoutManager().RunTimeout(timeout->When());
+  timeout->mWindow->AsInner()->TimeoutManager().RunTimeout(TimeStamp::Now(), timeout->When());
   timeout->mClosureSelfRef = nullptr;
 }
 
 void
 TimerNameCallback(nsITimer* aTimer, bool aAnonymize, void* aClosure,
                   char* aBuf, size_t aLen)
 {
   RefPtr<Timeout> timeout = (Timeout*)aClosure;
--- a/dom/base/TimeoutExecutor.cpp
+++ b/dom/base/TimeoutExecutor.cpp
@@ -133,17 +133,17 @@ TimeoutExecutor::MaybeExecute()
   // by TimeoutManager.
   TimeStamp now(TimeStamp::Now());
   if (deadline > now) {
     deadline = now;
   }
 
   Cancel();
 
-  mOwner->RunTimeout(deadline);
+  mOwner->RunTimeout(now, deadline);
 }
 
 TimeoutExecutor::TimeoutExecutor(TimeoutManager* aOwner)
   : mOwner(aOwner)
   , mMode(Mode::None)
 {
   MOZ_DIAGNOSTIC_ASSERT(mOwner);
 }
--- a/dom/base/TimeoutManager.cpp
+++ b/dom/base/TimeoutManager.cpp
@@ -8,16 +8,17 @@
 #include "nsGlobalWindow.h"
 #include "mozilla/Logging.h"
 #include "mozilla/Telemetry.h"
 #include "mozilla/ThrottledEventQueue.h"
 #include "mozilla/TimeStamp.h"
 #include "nsITimeoutHandler.h"
 #include "mozilla/dom/TabGroup.h"
 #include "OrderedTimeoutIterator.h"
+#include "TimeoutExecutor.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 static LazyLogModule gLog("Timeout");
 
 // Time between sampling timeout execution time.
 const uint32_t kTelemetryPeriodMS = 1000;
@@ -296,16 +297,17 @@ CalculateNewBackPressureDelayMS(uint32_t
   }
   return static_cast<int32_t>(value);
 }
 
 } // anonymous namespace
 
 TimeoutManager::TimeoutManager(nsGlobalWindow& aWindow)
   : mWindow(aWindow),
+    mExecutor(new TimeoutExecutor(this)),
     mTimeoutIdCounter(1),
     mNextFiringId(InvalidFiringId + 1),
     mRunningTimeout(nullptr),
     mIdleCallbackTimeoutCounter(1),
     mBackPressureDelayMS(0),
     mThrottleTrackingTimeouts(false)
 {
   MOZ_DIAGNOSTIC_ASSERT(aWindow.IsInnerWindow());
@@ -315,16 +317,18 @@ TimeoutManager::TimeoutManager(nsGlobalW
            this, gAnnotateTrackingChannels ? "enabled" : "disabled"));
 }
 
 TimeoutManager::~TimeoutManager()
 {
   MOZ_DIAGNOSTIC_ASSERT(mWindow.AsInner()->InnerObjectsFreed());
   MOZ_DIAGNOSTIC_ASSERT(!mThrottleTrackingTimeoutsTimer);
 
+  mExecutor->Shutdown();
+
   MOZ_LOG(gLog, LogLevel::Debug,
           ("TimeoutManager %p destroyed\n", this));
 }
 
 /* static */
 void
 TimeoutManager::Initialize()
 {
@@ -469,24 +473,17 @@ TimeoutManager::SetTimeout(nsITimeoutHan
 
   TimeDuration delta = TimeDuration::FromMilliseconds(realInterval);
   timeout->SetWhenOrTimeRemaining(TimeStamp::Now(), delta);
 
   // If we're not suspended, then set the timer.
   if (!mWindow.IsSuspended()) {
     MOZ_ASSERT(!timeout->When().IsNull());
 
-    nsresult rv;
-    timeout->mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
-    if (NS_FAILED(rv)) {
-      return rv;
-    }
-
-    rv = timeout->InitTimer(mWindow.EventTargetFor(TaskCategory::Timer),
-                            realInterval);
+    nsresult rv = mExecutor->MaybeSchedule(timeout->When());
     if (NS_FAILED(rv)) {
       return rv;
     }
   }
 
   if (!aIsInterval) {
     timeout->mNestingLevel = nestingLevel;
   }
@@ -540,43 +537,65 @@ TimeoutManager::SetTimeout(nsITimeoutHan
   return NS_OK;
 }
 
 void
 TimeoutManager::ClearTimeout(int32_t aTimerId, Timeout::Reason aReason)
 {
   uint32_t timerId = (uint32_t)aTimerId;
 
+  bool firstTimeout = true;
+
   ForEachUnorderedTimeoutAbortable([&](Timeout* aTimeout) {
     MOZ_LOG(gLog, LogLevel::Debug,
             ("Clear%s(TimeoutManager=%p, timeout=%p, aTimerId=%u, ID=%u, tracking=%d)\n", aTimeout->mIsInterval ? "Interval" : "Timeout",
              this, aTimeout, timerId, aTimeout->mTimeoutId,
              int(aTimeout->mIsTracking)));
 
     if (aTimeout->mTimeoutId == timerId && aTimeout->mReason == aReason) {
       if (aTimeout->mRunning) {
         /* We're running from inside the aTimeout. Mark this
            aTimeout for deferred deletion by the code in
            RunTimeout() */
         aTimeout->mIsInterval = false;
       }
       else {
         /* Delete the aTimeout from the pending aTimeout list */
-        aTimeout->MaybeCancelTimer();
         aTimeout->remove();
       }
       return true; // abort!
     }
+
+    firstTimeout = false;
+
     return false;
   });
+
+  if (!firstTimeout) {
+    return;
+  }
+
+  // If the first timeout was cancelled we need to stop the executor and
+  // restart at the next soonest deadline.
+  mExecutor->Cancel();
+
+  OrderedTimeoutIterator iter(mNormalTimeouts,
+                              mTrackingTimeouts,
+                              nullptr,
+                              nullptr);
+  Timeout* nextTimeout = iter.Next();
+  if (nextTimeout) {
+    MOZ_ALWAYS_SUCCEEDS(mExecutor->MaybeSchedule(nextTimeout->When()));
+  }
 }
 
 void
-TimeoutManager::RunTimeout(const TimeStamp& aTargetDeadline)
+TimeoutManager::RunTimeout(const TimeStamp& aNow, const TimeStamp& aTargetDeadline)
 {
+  MOZ_DIAGNOSTIC_ASSERT(!aNow.IsNull());
   MOZ_DIAGNOSTIC_ASSERT(!aTargetDeadline.IsNull());
 
   if (mWindow.IsSuspended()) {
     return;
   }
 
   NS_ASSERTION(!mWindow.IsFrozen(), "Timeout running on a window in the bfcache!");
 
@@ -590,17 +609,18 @@ TimeoutManager::RunTimeout(const TimeSta
     TimeDuration::FromMilliseconds(totalTimeLimit.ToMilliseconds() / 4);
 
   // Ammortize overhead from from calling TimeStamp::Now() in the initial
   // loop, though, by only checking for an elapsed limit every N timeouts.
   const uint32_t kNumTimersPerInitialElapsedCheck = 100;
 
   // Start measuring elapsed time immediately.  We won't potentially expire
   // the time budget until at least one Timeout has run, though.
-  TimeStamp start = TimeStamp::Now();
+  TimeStamp now(aNow);
+  TimeStamp start = now;
 
   Timeout* last_expired_normal_timeout = nullptr;
   Timeout* last_expired_tracking_timeout = nullptr;
   bool     last_expired_timeout_is_normal = false;
   Timeout* last_normal_insertion_point = nullptr;
   Timeout* last_tracking_insertion_point = nullptr;
 
   uint32_t firingId = CreateFiringId();
@@ -613,30 +633,31 @@ TimeoutManager::RunTimeout(const TimeSta
   nsCOMPtr<nsIScriptGlobalObject> windowKungFuDeathGrip(&mWindow);
   // Silence the static analysis error about windowKungFuDeathGrip.  Accessing
   // members of mWindow here is safe, because the lifetime of TimeoutManager is
   // the same as the lifetime of the containing nsGlobalWindow.
   Unused << windowKungFuDeathGrip;
 
   // A native timer has gone off. See which of our timeouts need
   // servicing
-  TimeStamp now = TimeStamp::Now();
   TimeStamp deadline;
 
   if (aTargetDeadline > now) {
     // The OS timer fired early (which can happen due to the timers
     // having lower precision than TimeStamp does).  Set |deadline| to
     // be the time when the OS timer *should* have fired so that any
     // timers that *should* have fired *will* be fired now.
 
     deadline = aTargetDeadline;
   } else {
     deadline = now;
   }
 
+  TimeStamp nextDeadline;
+
   // The timeout list is kept in deadline order. Discover the latest timeout
   // whose deadline has expired. On some platforms, native timeout events fire
   // "early", but we handled that above by setting deadline to aTargetDeadline
   // if the timer fired early.  So we can stop walking if we get to timeouts
   // whose When() is greater than deadline, since once that happens we know
   // nothing past that point is expired.
   {
     // Use a nested scope in order to make sure the strong references held by
@@ -646,16 +667,19 @@ TimeoutManager::RunTimeout(const TimeSta
                                        nullptr,
                                        nullptr);
 
     uint32_t numTimersToRun = 0;
 
     while (true) {
       Timeout* timeout = expiredIter.Next();
       if (!timeout || timeout->When() > deadline) {
+        if (timeout) {
+          nextDeadline = timeout->When();
+        }
         break;
       }
 
       if (IsInvalidFiringId(timeout->mFiringId)) {
         // Mark any timeouts that are on the list to be fired with the
         // firing depth so that we can reentrantly run timeouts
         timeout->mFiringId = firingId;
         last_expired_timeout_is_normal = expiredIter.PickedNormalIter();
@@ -664,27 +688,40 @@ TimeoutManager::RunTimeout(const TimeSta
         } else {
           last_expired_tracking_timeout = timeout;
         }
 
         numTimersToRun += 1;
 
         // Run only a limited number of timers based on the configured maximum.
         if (numTimersToRun % kNumTimersPerInitialElapsedCheck == 0) {
-          TimeDuration elapsed(TimeStamp::Now() - start);
+          now = TimeStamp::Now();
+          TimeDuration elapsed(now - start);
           if (elapsed >= initalTimeLimit) {
+            nextDeadline = timeout->When();
             break;
           }
         }
       }
 
       expiredIter.UpdateIterator();
     }
   }
 
+  now = TimeStamp::Now();
+
+  // Wherever we stopped in the timer list, schedule the executor to
+  // run for the next unexpired deadline.  Note, this *must* be done
+  // before we start executing any content script handlers.  If one
+  // of them spins the event loop the executor must already be scheduled
+  // in order for timeouts to fire properly.
+  if (!nextDeadline.IsNull()) {
+    MOZ_ALWAYS_SUCCEEDS(mExecutor->MaybeSchedule(nextDeadline));
+  }
+
   // Maybe the timeout that the event was fired for has been deleted
   // and there are no others timeouts with deadlines that make them
   // eligible for execution yet. Go away.
   if (!last_expired_normal_timeout && !last_expired_tracking_timeout) {
     return;
   }
 
   // Insert a dummy timeout into the list of timeouts between the
@@ -793,16 +830,18 @@ TimeoutManager::RunTimeout(const TimeSta
         mTrackingTimeouts.SetInsertionPoint(last_tracking_insertion_point);
 
         // Since ClearAllTimeouts() was called the lists should be empty.
         MOZ_DIAGNOSTIC_ASSERT(!HasTimeouts());
 
         return;
       }
 
+      now = TimeStamp::Now();
+
       // If we have a regular interval timer, we re-schedule the
       // timeout, accounting for clock drift.
       bool needsReinsertion = RescheduleTimeout(timeout, now);
 
       // Running a timeout can cause another timeout to be deleted, so
       // we need to reset the pointer to the following timeout.
       runIter.UpdateIterator();
 
@@ -819,18 +858,24 @@ TimeoutManager::RunTimeout(const TimeSta
           mNormalTimeouts.Insert(timeout,
                                  mWindow.IsFrozen() ? Timeouts::SortBy::TimeRemaining
                                                     : Timeouts::SortBy::TimeWhen);
         }
       }
 
       // Check to see if we have run out of time to execute timeout handlers.
       // If we've exceeded our time budget then terminate the loop immediately.
-      TimeDuration elapsed = TimeStamp::Now() - start;
+      TimeDuration elapsed = now - start;
       if (elapsed >= totalTimeLimit) {
+        // We ran out of time.  Make sure to schedule the executor to
+        // run immediately for the next timer, if it exists.
+        RefPtr<Timeout> timeout = runIter.Next();
+        if (timeout) {
+          MOZ_ALWAYS_SUCCEEDS(mExecutor->MaybeSchedule(timeout->When()));
+        }
         break;
       }
     }
   }
 
   // Take the dummy timeout off the head of the list
   if (dummy_normal_timeout->isInList()) {
     dummy_normal_timeout->remove();
@@ -955,20 +1000,16 @@ TimeoutManager::CancelOrUpdateBackPressu
       &TimeoutManager::CancelOrUpdateBackPressure, &mWindow);
   MOZ_ALWAYS_SUCCEEDS(queue->Dispatch(r.forget(), NS_DISPATCH_NORMAL));
 }
 
 bool
 TimeoutManager::RescheduleTimeout(Timeout* aTimeout, const TimeStamp& now)
 {
   if (!aTimeout->mIsInterval) {
-    // The timeout still has an OS timer, and it's not an interval,
-    // that means that the OS timer could still fire; cancel the OS
-    // timer and release its reference to the timeout.
-    aTimeout->MaybeCancelTimer();
     return false;
   }
 
   // Compute time to next timeout for interval timer.
   // Make sure nextInterval is at least DOMMinTimeoutValue().
   TimeDuration nextInterval =
     TimeDuration::FromMilliseconds(
         std::max(aTimeout->mInterval,
@@ -983,39 +1024,22 @@ TimeoutManager::RescheduleTimeout(Timeou
   // thread is firing our timers somewhat early or if they're taking a long
   // time to run the callback.
   if (delay < TimeDuration(0)) {
     delay = TimeDuration(0);
   }
 
   aTimeout->SetWhenOrTimeRemaining(currentNow, delay);
 
-  if (!aTimeout->mTimer) {
-    MOZ_DIAGNOSTIC_ASSERT(mWindow.IsFrozen() || mWindow.IsSuspended());
+  if (mWindow.IsSuspended()) {
     return true;
   }
 
-  // Reschedule the OS timer. Don't bother returning any error codes if
-  // this fails since the callers of this method don't care about them.
-  nsresult rv = aTimeout->InitTimer(mWindow.EventTargetFor(TaskCategory::Timer),
-                                    delay.ToMilliseconds());
-
-  if (NS_FAILED(rv)) {
-    NS_ERROR("Error initializing timer for DOM timeout!");
-
-    // We failed to initialize the new OS timer, this timer does
-    // us no good here so we just cancel it (just in case) and
-    // null out the pointer to the OS timer, this will release the
-    // OS timer. As we continue executing the code below we'll end
-    // up deleting the timeout since it's not an interval timeout
-    // any more (since timeout->mTimer == nullptr).
-    aTimeout->MaybeCancelTimer();
-
-    return false;
-  }
+  nsresult rv = mExecutor->MaybeSchedule(aTimeout->When());
+  NS_ENSURE_SUCCESS(rv, false);
 
   return true;
 }
 
 nsresult
 TimeoutManager::ResetTimersForThrottleReduction()
 {
   return ResetTimersForThrottleReduction(gMinBackgroundTimeoutValue);
@@ -1028,36 +1052,42 @@ TimeoutManager::ResetTimersForThrottleRe
 
   if (mWindow.IsFrozen() || mWindow.IsSuspended()) {
     return NS_OK;
   }
 
   Timeouts::SortBy sortBy = mWindow.IsFrozen() ? Timeouts::SortBy::TimeRemaining
                                                : Timeouts::SortBy::TimeWhen;
 
-  nsCOMPtr<nsIEventTarget> queue = mWindow.EventTargetFor(TaskCategory::Timer);
   nsresult rv = mNormalTimeouts.ResetTimersForThrottleReduction(aPreviousThrottleDelayMS,
                                                                 *this,
-                                                                sortBy,
-                                                                queue);
+                                                                sortBy);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = mTrackingTimeouts.ResetTimersForThrottleReduction(aPreviousThrottleDelayMS,
                                                          *this,
-                                                         sortBy,
-                                                         queue);
+                                                         sortBy);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  OrderedTimeoutIterator iter(mNormalTimeouts,
+                              mTrackingTimeouts,
+                              nullptr,
+                              nullptr);
+  Timeout* firstTimeout = iter.Next();
+  if (firstTimeout) {
+    rv = mExecutor->MaybeSchedule(firstTimeout->When());
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
   return NS_OK;
 }
 
 nsresult
 TimeoutManager::Timeouts::ResetTimersForThrottleReduction(int32_t aPreviousThrottleDelayMS,
                                                           const TimeoutManager& aTimeoutManager,
-                                                          SortBy aSortBy,
-                                                          nsIEventTarget* aQueue)
+                                                          SortBy aSortBy)
 {
   TimeStamp now = TimeStamp::Now();
 
   // If insertion point is non-null, we're in the middle of firing timers and
   // the timers we're planning to fire all come before insertion point;
   // insertion point itself is a dummy timeout with an When() that may be
   // semi-bogus.  In that case, we don't need to do anything with insertion
   // point or anything before it, so should start at the timer after insertion
@@ -1126,23 +1156,16 @@ TimeoutManager::Timeouts::ResetTimersFor
                      timeout->When() < nextTimeout->When(), "How did that happen?");
         timeout->remove();
         // Insert() will reset mFiringId. Make sure to undo that.
         uint32_t firingId = timeout->mFiringId;
         Insert(timeout, aSortBy);
         timeout->mFiringId = firingId;
       }
 
-      nsresult rv = timeout->InitTimer(aQueue, delay.ToMilliseconds());
-
-      if (NS_FAILED(rv)) {
-        NS_WARNING("Error resetting non background timer for DOM timeout!");
-        return rv;
-      }
-
       timeout = nextTimeout;
     } else {
       timeout = timeout->getNext();
     }
   }
 
   return NS_OK;
 }
@@ -1155,28 +1178,28 @@ TimeoutManager::ClearAllTimeouts()
   MOZ_LOG(gLog, LogLevel::Debug,
           ("ClearAllTimeouts(TimeoutManager=%p)\n", this));
 
   if (mThrottleTrackingTimeoutsTimer) {
     mThrottleTrackingTimeoutsTimer->Cancel();
     mThrottleTrackingTimeoutsTimer = nullptr;
   }
 
+  mExecutor->Cancel();
+
   ForEachUnorderedTimeout([&](Timeout* aTimeout) {
     /* If RunTimeout() is higher up on the stack for this
        window, e.g. as a result of document.write from a timeout,
        then we need to reset the list insertion point for
        newly-created timeouts in case the user adds a timeout,
        before we pop the stack back to RunTimeout. */
     if (mRunningTimeout == aTimeout) {
       seenRunningTimeout = true;
     }
 
-    aTimeout->MaybeCancelTimer();
-
     // Set timeout->mCleared to true to indicate that the timeout was
     // cleared and taken out of the list of timeouts
     aTimeout->mCleared = true;
   });
 
   if (seenRunningTimeout) {
     mNormalTimeouts.SetInsertionPoint(nullptr);
     mTrackingTimeouts.SetInsertionPoint(nullptr);
@@ -1274,24 +1297,17 @@ TimeoutManager::Suspend()
   MOZ_LOG(gLog, LogLevel::Debug,
           ("Suspend(TimeoutManager=%p)\n", this));
 
   if (mThrottleTrackingTimeoutsTimer) {
     mThrottleTrackingTimeoutsTimer->Cancel();
     mThrottleTrackingTimeoutsTimer = nullptr;
   }
 
-  ForEachUnorderedTimeout([](Timeout* aTimeout) {
-    // Leave the timers with the current time remaining.  This will
-    // cause the timers to potentially fire when the window is
-    // Resume()'d.  Time effectively passes while suspended.
-
-    // Drop the XPCOM timer; we'll reschedule when restoring the state.
-    aTimeout->MaybeCancelTimer();
-  });
+  mExecutor->Cancel();
 }
 
 void
 TimeoutManager::Resume()
 {
   MOZ_LOG(gLog, LogLevel::Debug,
           ("Resume(TimeoutManager=%p)\n", this));
 
@@ -1300,54 +1316,47 @@ TimeoutManager::Resume()
   // again.
   if (mWindow.AsInner()->IsDocumentLoaded() && !mThrottleTrackingTimeouts) {
     MaybeStartThrottleTrackingTimout();
   }
 
   TimeStamp now = TimeStamp::Now();
   DebugOnly<bool> _seenDummyTimeout = false;
 
+  TimeStamp nextWakeUp;
+
   ForEachUnorderedTimeout([&](Timeout* aTimeout) {
     // There's a chance we're being called with RunTimeout on the stack in which
     // case we have a dummy timeout in the list that *must not* be resumed. It
     // can be identified by a null mWindow.
     if (!aTimeout->mWindow) {
       NS_ASSERTION(!_seenDummyTimeout, "More than one dummy timeout?!");
       _seenDummyTimeout = true;
       return;
     }
 
-    MOZ_ASSERT(!aTimeout->mTimer);
-
     // The timeout When() is set to the absolute time when the timer should
     // fire.  Recalculate the delay from now until that deadline.  If the
     // the deadline has already passed or falls within our minimum delay
-    // deadline, then clamp the resulting value to the minimum delay.  The
-    // When() will remain at its absolute time, but we won'aTimeout fire the OS
-    // timer until our calculated delay has passed.
+    // deadline, then clamp the resulting value to the minimum delay.
     int32_t remaining = 0;
     if (aTimeout->When() > now) {
       remaining = static_cast<int32_t>((aTimeout->When() - now).ToMilliseconds());
     }
     uint32_t delay = std::max(remaining, DOMMinTimeoutValue(aTimeout->mIsTracking));
-
-    aTimeout->mTimer = do_CreateInstance("@mozilla.org/timer;1");
-    if (!aTimeout->mTimer) {
-      aTimeout->remove();
-      return;
-    }
+    aTimeout->SetWhenOrTimeRemaining(now, TimeDuration::FromMilliseconds(delay));
 
-    nsresult rv = aTimeout->InitTimer(mWindow.EventTargetFor(TaskCategory::Timer),
-                                      delay);
-    if (NS_FAILED(rv)) {
-      aTimeout->mTimer = nullptr;
-      aTimeout->remove();
-      return;
+    if (nextWakeUp.IsNull() || aTimeout->When() < nextWakeUp) {
+      nextWakeUp = aTimeout->When();
     }
   });
+
+  if (!nextWakeUp.IsNull()) {
+    MOZ_ALWAYS_SUCCEEDS(mExecutor->MaybeSchedule(nextWakeUp));
+  }
 }
 
 void
 TimeoutManager::Freeze()
 {
   MOZ_LOG(gLog, LogLevel::Debug,
           ("Freeze(TimeoutManager=%p)\n", this));
 
@@ -1366,20 +1375,16 @@ TimeoutManager::Freeze()
     // shifts timers to the right as if time does not pass while
     // the window is frozen.
     TimeDuration delta(0);
     if (aTimeout->When() > now) {
       delta = aTimeout->When() - now;
     }
     aTimeout->SetWhenOrTimeRemaining(now, delta);
     MOZ_DIAGNOSTIC_ASSERT(aTimeout->TimeRemaining() == delta);
-
-    // Since we are suspended there should be no OS timer set for
-    // this timeout entry.
-    MOZ_ASSERT(!aTimeout->mTimer);
   });
 }
 
 void
 TimeoutManager::Thaw()
 {
   MOZ_LOG(gLog, LogLevel::Debug,
           ("Thaw(TimeoutManager=%p)\n", this));
@@ -1395,18 +1400,16 @@ TimeoutManager::Thaw()
       NS_ASSERTION(!_seenDummyTimeout, "More than one dummy timeout?!");
       _seenDummyTimeout = true;
       return;
     }
 
     // Set When() back to the time when the timer is supposed to fire.
     aTimeout->SetWhenOrTimeRemaining(now, aTimeout->TimeRemaining());
     MOZ_DIAGNOSTIC_ASSERT(!aTimeout->When().IsNull());
-
-    MOZ_ASSERT(!aTimeout->mTimer);
   });
 }
 
 bool
 TimeoutManager::IsTimeoutTracking(uint32_t aTimeoutId)
 {
   return mTrackingTimeouts.ForEachAbortable([&](Timeout* aTimeout) {
       return aTimeout->mTimeoutId == aTimeoutId;
--- a/dom/base/TimeoutManager.h
+++ b/dom/base/TimeoutManager.h
@@ -13,16 +13,17 @@
 class nsIEventTarget;
 class nsITimeoutHandler;
 class nsGlobalWindow;
 
 namespace mozilla {
 namespace dom {
 
 class OrderedTimeoutIterator;
+class TimeoutExecutor;
 
 // This class manages the timeouts in a Window's setTimeout/setInterval pool.
 class TimeoutManager final
 {
 public:
   explicit TimeoutManager(nsGlobalWindow& aWindow);
   ~TimeoutManager();
   TimeoutManager(const TimeoutManager& rhs) = delete;
@@ -42,17 +43,17 @@ public:
   nsresult SetTimeout(nsITimeoutHandler* aHandler,
                       int32_t interval, bool aIsInterval,
                       mozilla::dom::Timeout::Reason aReason,
                       int32_t* aReturn);
   void ClearTimeout(int32_t aTimerId,
                     mozilla::dom::Timeout::Reason aReason);
 
   // The timeout implementation functions.
-  void RunTimeout(const TimeStamp& aTargetDeadline);
+  void RunTimeout(const TimeStamp& aNow, const TimeStamp& aTargetDeadline);
   // Return true if |aTimeout| needs to be reinserted into the timeout list.
   bool RescheduleTimeout(mozilla::dom::Timeout* aTimeout, const TimeStamp& now);
 
   void ClearAllTimeouts();
   uint32_t GetTimeoutId(mozilla::dom::Timeout::Reason aReason);
 
   // Apply back pressure to the window if the TabGroup ThrottledEventQueue
   // exists and has too many runnables waiting to run.  For example, increase
@@ -150,18 +151,17 @@ private:
     enum class SortBy
     {
       TimeRemaining,
       TimeWhen
     };
     void Insert(mozilla::dom::Timeout* aTimeout, SortBy aSortBy);
     nsresult ResetTimersForThrottleReduction(int32_t aPreviousThrottleDelayMS,
                                              const TimeoutManager& aTimeoutManager,
-                                             SortBy aSortBy,
-                                             nsIEventTarget* aQueue);
+                                             SortBy aSortBy);
 
     const Timeout* GetFirst() const { return mTimeoutList.getFirst(); }
     Timeout* GetFirst() { return mTimeoutList.getFirst(); }
     const Timeout* GetLast() const { return mTimeoutList.getLast(); }
     Timeout* GetLast() { return mTimeoutList.getLast(); }
     bool IsEmpty() const { return mTimeoutList.isEmpty(); }
     void InsertFront(Timeout* aTimeout) { mTimeoutList.insertFront(aTimeout); }
     void Clear() { mTimeoutList.clear(); }
@@ -215,16 +215,20 @@ private:
     mozilla::dom::Timeout*    mTimeoutInsertionPoint;
   };
 
   friend class OrderedTimeoutIterator;
 
   // Each nsGlobalWindow object has a TimeoutManager member.  This reference
   // points to that holder object.
   nsGlobalWindow&             mWindow;
+  // The executor is specific to the nsGlobalWindow/TimeoutManager, but it
+  // can live past the destruction of the window if its scheduled.  Therefore
+  // it must be a separate ref-counted object.
+  RefPtr<TimeoutExecutor>     mExecutor;
   // The list of timeouts coming from non-tracking scripts.
   Timeouts                    mNormalTimeouts;
   // The list of timeouts coming from scripts on the tracking protection list.
   Timeouts                    mTrackingTimeouts;
   uint32_t                    mTimeoutIdCounter;
   uint32_t                    mNextFiringId;
   AutoTArray<uint32_t, 2>     mFiringIdStack;
   mozilla::dom::Timeout*      mRunningTimeout;