Bug 1270059: Defer setTimeout/Intervals()s in content during pageload r=smaug
authorRandell Jesup <rjesup@jesup.org>
Sat, 26 Jan 2019 12:18:06 -0500
changeset 512686 7f8594d21c51ba9966c85b1bacbf0d245e45395f
parent 512685 65cf08e33fe2b12a90a505462f3246df204c64ad
child 512687 eec1a6963a9cc61b02611259233f54fd15094792
push id10566
push userarchaeopteryx@coole-files.de
push dateMon, 28 Jan 2019 12:41:12 +0000
treeherdermozilla-beta@69a3d7c8d04b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1270059
milestone66.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 1270059: Defer setTimeout/Intervals()s in content during pageload r=smaug They will run off the new DeferredTimers idle queue if idle time is available
dom/base/Document.cpp
dom/base/Document.h
dom/base/Timeout.cpp
dom/base/Timeout.h
dom/base/TimeoutExecutor.cpp
dom/base/TimeoutExecutor.h
dom/base/TimeoutManager.cpp
dom/base/TimeoutManager.h
dom/base/nsGlobalWindowInner.cpp
dom/base/nsGlobalWindowOuter.cpp
dom/base/nsPIDOMWindow.h
modules/libpref/init/StaticPrefList.h
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -1290,16 +1290,17 @@ Document::Document(const char* aContentT
 #endif
       mHasDelayedRefreshEvent(false),
       mPendingFullscreenRequests(0),
       mXMLDeclarationBits(0),
       mOnloadBlockCount(0),
       mAsyncOnloadBlockCount(0),
       mCompatMode(eCompatibility_FullStandards),
       mReadyState(ReadyState::READYSTATE_UNINITIALIZED),
+      mAncestorIsLoading(false),
 #ifdef MOZILLA_INTERNAL_API
       mVisibilityState(dom::VisibilityState::Hidden),
 #else
       mDummy(0),
 #endif
       mType(eUnknown),
       mDefaultElementType(0),
       mAllowXULXBL(eTriUnset),
@@ -8111,24 +8112,62 @@ nsresult Document::CloneDocHelper(Docume
   // State from Document
   clone->mType = mType;
   clone->mXMLDeclarationBits = mXMLDeclarationBits;
   clone->mBaseTarget = mBaseTarget;
 
   return NS_OK;
 }
 
+static bool SetLoadingInSubDocument(Document* aDocument, void* aData) {
+  aDocument->SetAncestorLoading(*(static_cast<bool*>(aData)));
+  return true;
+}
+
+void Document::SetAncestorLoading(bool aAncestorIsLoading) {
+  NotifyLoading(mAncestorIsLoading, aAncestorIsLoading, mReadyState,
+                mReadyState);
+  mAncestorIsLoading = aAncestorIsLoading;
+}
+
+void Document::NotifyLoading(const bool& aCurrentParentIsLoading,
+                             bool aNewParentIsLoading,
+                             const ReadyState& aCurrentState,
+                             ReadyState aNewState) {
+  // Mirror the top-level loading state down to all subdocuments
+  bool was_loading = aCurrentParentIsLoading ||
+                     aCurrentState == READYSTATE_LOADING ||
+                     aCurrentState == READYSTATE_INTERACTIVE;
+  bool is_loading = aNewParentIsLoading || aNewState == READYSTATE_LOADING ||
+                    aNewState == READYSTATE_INTERACTIVE;  // new value for state
+  bool set_load_state = was_loading != is_loading;
+
+  if (set_load_state && StaticPrefs::dom_timeout_defer_during_load()) {
+    nsPIDOMWindowInner* inner = GetInnerWindow();
+    if (inner) {
+      inner->SetActiveLoadingState(is_loading);
+    }
+    EnumerateSubDocuments(SetLoadingInSubDocument, &is_loading);
+  }
+}
+
 void Document::SetReadyStateInternal(ReadyState rs) {
-  mReadyState = rs;
   if (rs == READYSTATE_UNINITIALIZED) {
     // Transition back to uninitialized happens only to keep assertions happy
     // right before readyState transitions to something else. Make this
     // transition undetectable by Web content.
-    return;
-  }
+    mReadyState = rs;
+    return;
+  }
+
+  if (READYSTATE_LOADING == rs) {
+    mLoadingTimeStamp = mozilla::TimeStamp::Now();
+  }
+  NotifyLoading(mAncestorIsLoading, mAncestorIsLoading, mReadyState, rs);
+  mReadyState = rs;
   if (mTiming) {
     switch (rs) {
       case READYSTATE_LOADING:
         mTiming->NotifyDOMLoading(Document::GetDocumentURI());
         break;
       case READYSTATE_INTERACTIVE:
         mTiming->NotifyDOMInteractive(Document::GetDocumentURI());
         break;
@@ -8136,19 +8175,16 @@ void Document::SetReadyStateInternal(Rea
         mTiming->NotifyDOMComplete(Document::GetDocumentURI());
         break;
       default:
         NS_WARNING("Unexpected ReadyState value");
         break;
     }
   }
   // At the time of loading start, we don't have timing object, record time.
-  if (READYSTATE_LOADING == rs) {
-    mLoadingTimeStamp = mozilla::TimeStamp::Now();
-  }
 
   if (READYSTATE_INTERACTIVE == rs) {
     if (nsContentUtils::IsSystemPrincipal(NodePrincipal())) {
       Element* root = GetRootElement();
       if (root && root->HasAttr(kNameSpaceID_None, nsGkAtoms::mozpersist)) {
         mXULPersist = new XULPersist(this);
         mXULPersist->Init();
       }
--- a/dom/base/Document.h
+++ b/dom/base/Document.h
@@ -1898,16 +1898,21 @@ class Document : public nsINode,
     READYSTATE_UNINITIALIZED = 0,
     READYSTATE_LOADING = 1,
     READYSTATE_INTERACTIVE = 3,
     READYSTATE_COMPLETE = 4
   };
   void SetReadyStateInternal(ReadyState rs);
   ReadyState GetReadyStateEnum() { return mReadyState; }
 
+  void SetAncestorLoading(bool aAncestorIsLoading);
+  void NotifyLoading(const bool& aCurrentParentIsLoading,
+                     bool aNewParentIsLoading, const ReadyState& aCurrentState,
+                     ReadyState aNewState);
+
   // notify that a content node changed state.  This must happen under
   // a scriptblocker but NOT within a begin/end update.
   void ContentStateChanged(nsIContent* aContent,
                            mozilla::EventStates aStateMask);
 
   // Notify that a document state has changed.
   // This should only be called by callers whose state is also reflected in the
   // implementation of Document::GetDocumentState.
@@ -4111,16 +4116,19 @@ class Document : public nsINode,
   uint32_t mAsyncOnloadBlockCount;
 
   // Compatibility mode
   nsCompatibility mCompatMode;
 
   // Our readyState
   ReadyState mReadyState;
 
+  // Ancestor's loading state
+  bool mAncestorIsLoading;
+
 #ifdef MOZILLA_INTERNAL_API
   // Our visibility state
   mozilla::dom::VisibilityState mVisibilityState;
   static_assert(sizeof(mozilla::dom::VisibilityState) == sizeof(uint8_t),
                 "Error size of mVisibilityState and mDummy");
 #else
   uint8_t mDummy;
 #endif
--- a/dom/base/Timeout.cpp
+++ b/dom/base/Timeout.cpp
@@ -38,16 +38,17 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(Timeout, AddRef)
 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(Timeout, Release)
 
 void Timeout::SetWhenOrTimeRemaining(const TimeStamp& aBaseTime,
                                      const TimeDuration& aDelay) {
   MOZ_DIAGNOSTIC_ASSERT(mWindow);
+  mSubmitTime = aBaseTime;
 
   // If we are frozen simply set mTimeRemaining to be the "time remaining" in
   // the timeout (i.e., the interval itself).  This will be used to create a
   // new mWhen time when the window is thawed.  The end effect is that time does
   // not appear to pass for frozen windows.
   if (mWindow->IsFrozen()) {
     mWhen = TimeStamp();
     mTimeRemaining = aDelay;
@@ -63,16 +64,18 @@ void Timeout::SetWhenOrTimeRemaining(con
 
 const TimeStamp& Timeout::When() const {
   MOZ_DIAGNOSTIC_ASSERT(!mWhen.IsNull());
   // Note, mWindow->IsFrozen() can be true here.  The Freeze() method calls
   // When() to calculate the delay to populate mTimeRemaining.
   return mWhen;
 }
 
+const TimeStamp& Timeout::SubmitTime() const { return mSubmitTime; }
+
 const TimeDuration& Timeout::TimeRemaining() const {
   MOZ_DIAGNOSTIC_ASSERT(mWhen.IsNull());
   // Note, mWindow->IsFrozen() can be false here.  The Thaw() method calls
   // TimeRemaining() to calculate the new When() value.
   return mTimeRemaining;
 }
 
 }  // namespace dom
--- a/dom/base/Timeout.h
+++ b/dom/base/Timeout.h
@@ -40,29 +40,36 @@ class Timeout final : public LinkedListE
   };
 
   void SetWhenOrTimeRemaining(const TimeStamp& aBaseTime,
                               const TimeDuration& aDelay);
 
   // Can only be called when not frozen.
   const TimeStamp& When() const;
 
+  const TimeStamp& SubmitTime() const;
+
   // Can only be called when frozen.
   const TimeDuration& TimeRemaining() const;
 
  private:
   // mWhen and mTimeRemaining can't be in a union, sadly, because they
   // have constructors.
   // Nominal time to run this timeout.  Use only when timeouts are not
   // frozen.
   TimeStamp mWhen;
 
   // Remaining time to wait.  Used only when timeouts are frozen.
   TimeDuration mTimeRemaining;
 
+  // Time that the timeout started, restarted, or was frozen.  Useful for
+  // logging time from (virtual) start of a timer until the time it fires
+  // (or is cancelled, etc)
+  TimeStamp mSubmitTime;
+
   ~Timeout() = default;
 
  public:
   // Public member variables in this section.  Please don't add to this list
   // or mix methods with these.  The interleaving public/private sections
   // is necessary as we migrate members to private while still trying to
   // keep decent binary packing.
 
--- a/dom/base/TimeoutExecutor.cpp
+++ b/dom/base/TimeoutExecutor.cpp
@@ -6,16 +6,18 @@
 
 #include "TimeoutExecutor.h"
 
 #include "mozilla/dom/TimeoutManager.h"
 #include "nsComponentManagerUtils.h"
 #include "nsIEventTarget.h"
 #include "nsString.h"
 
+extern mozilla::LazyLogModule gTimeoutLog;
+
 namespace mozilla {
 namespace dom {
 
 NS_IMPL_ISUPPORTS(TimeoutExecutor, nsIRunnable, nsITimerCallback, nsINamed)
 
 TimeoutExecutor::~TimeoutExecutor() {
   // The TimeoutManager should keep the Executor alive until its destroyed,
   // and then call Shutdown() explicitly.
@@ -25,18 +27,25 @@ TimeoutExecutor::~TimeoutExecutor() {
 }
 
 nsresult TimeoutExecutor::ScheduleImmediate(const TimeStamp& aDeadline,
                                             const TimeStamp& aNow) {
   MOZ_DIAGNOSTIC_ASSERT(mDeadline.IsNull());
   MOZ_DIAGNOSTIC_ASSERT(mMode == Mode::None);
   MOZ_DIAGNOSTIC_ASSERT(aDeadline <= (aNow + mAllowedEarlyFiringTime));
 
-  nsresult rv =
-      mOwner->EventTarget()->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL);
+  nsresult rv;
+  if (mIsIdleQueue) {
+    RefPtr<TimeoutExecutor> runnable(this);
+    MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Starting IdleDispatch runnable"));
+    rv = NS_DispatchToCurrentThreadQueue(runnable.forget(), mMaxIdleDeferMS,
+                                         EventQueuePriority::DeferredTimers);
+  } else {
+    rv = mOwner->EventTarget()->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL);
+  }
   NS_ENSURE_SUCCESS(rv, rv);
 
   mMode = Mode::Immediate;
   mDeadline = aDeadline;
 
   return NS_OK;
 }
 
@@ -45,24 +54,35 @@ nsresult TimeoutExecutor::ScheduleDelaye
                                           const TimeDuration& aMinDelay) {
   MOZ_DIAGNOSTIC_ASSERT(mDeadline.IsNull());
   MOZ_DIAGNOSTIC_ASSERT(mMode == Mode::None);
   MOZ_DIAGNOSTIC_ASSERT(!aMinDelay.IsZero() ||
                         aDeadline > (aNow + mAllowedEarlyFiringTime));
 
   nsresult rv = NS_OK;
 
+  if (mIsIdleQueue) {
+    // Nothing goes into the idletimeouts list if it wasn't going to
+    // fire at that time, so we can always schedule idle-execution of
+    // these immediately
+    return ScheduleImmediate(aNow, aNow);
+  }
+
   if (!mTimer) {
     mTimer = NS_NewTimer(mOwner->EventTarget());
     NS_ENSURE_TRUE(mTimer, NS_ERROR_OUT_OF_MEMORY);
 
     uint32_t earlyMicros = 0;
     MOZ_ALWAYS_SUCCEEDS(
         mTimer->GetAllowedEarlyFiringMicroseconds(&earlyMicros));
     mAllowedEarlyFiringTime = TimeDuration::FromMicroseconds(earlyMicros);
+    // Re-evaluate if we should have scheduled this immediately
+    if (aDeadline <= (aNow + mAllowedEarlyFiringTime)) {
+      return ScheduleImmediate(aDeadline, aNow);
+    }
   } else {
     // Always call Cancel() in case we are re-using a timer.
     rv = mTimer->Cancel();
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   // Calculate the delay based on the deadline and current time.  If we have
   // a minimum delay set then clamp to that value.
@@ -149,21 +169,25 @@ void TimeoutExecutor::MaybeExecute() {
   TimeStamp now(TimeStamp::Now());
   TimeStamp limit = now + mAllowedEarlyFiringTime;
   if (deadline > limit) {
     deadline = limit;
   }
 
   Cancel();
 
-  mOwner->RunTimeout(now, deadline);
+  mOwner->RunTimeout(now, deadline, mIsIdleQueue);
 }
 
-TimeoutExecutor::TimeoutExecutor(TimeoutManager* aOwner)
-    : mOwner(aOwner), mMode(Mode::None) {
+TimeoutExecutor::TimeoutExecutor(TimeoutManager* aOwner, bool aIsIdleQueue,
+                                 uint32_t aMaxIdleDeferMS)
+    : mOwner(aOwner),
+      mIsIdleQueue(aIsIdleQueue),
+      mMaxIdleDeferMS(aMaxIdleDeferMS),
+      mMode(Mode::None) {
   MOZ_DIAGNOSTIC_ASSERT(mOwner);
 }
 
 void TimeoutExecutor::Shutdown() {
   mOwner = nullptr;
 
   if (mTimer) {
     mTimer->Cancel();
@@ -196,16 +220,18 @@ void TimeoutExecutor::Cancel() {
   mMode = Mode::None;
   mDeadline = TimeStamp();
 }
 
 NS_IMETHODIMP
 TimeoutExecutor::Run() {
   // If the executor is canceled and then rescheduled its possible to get
   // spurious executions here.  Ignore these unless our current mode matches.
+  MOZ_LOG(gTimeoutLog, LogLevel::Debug,
+          ("Running Immediate %stimers", mIsIdleQueue ? "Idle" : ""));
   if (mMode == Mode::Immediate) {
     MaybeExecute();
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 TimeoutExecutor::Notify(nsITimer* aTimer) {
--- a/dom/base/TimeoutExecutor.h
+++ b/dom/base/TimeoutExecutor.h
@@ -17,18 +17,20 @@ namespace mozilla {
 namespace dom {
 
 class TimeoutManager;
 
 class TimeoutExecutor final : public nsIRunnable,
                               public nsITimerCallback,
                               public nsINamed {
   TimeoutManager* mOwner;
+  bool mIsIdleQueue;
   nsCOMPtr<nsITimer> mTimer;
   TimeStamp mDeadline;
+  uint32_t mMaxIdleDeferMS;
 
   // Limits how far we allow timers to fire into the future from their
   // deadline.  Starts off at zero, but is then adjusted when we start
   // using nsITimer.  The nsITimer implementation may sometimes fire
   // early and we should allow that to minimize additional wakeups.
   TimeDuration mAllowedEarlyFiringTime;
 
   // The TimeoutExecutor is repeatedly scheduled by the TimeoutManager
@@ -62,17 +64,18 @@ class TimeoutExecutor final : public nsI
   nsresult Schedule(const TimeStamp& aDeadline, const TimeDuration& aMinDelay);
 
   nsresult MaybeReschedule(const TimeStamp& aDeadline,
                            const TimeDuration& aMinDelay);
 
   void MaybeExecute();
 
  public:
-  explicit TimeoutExecutor(TimeoutManager* aOwner);
+  TimeoutExecutor(TimeoutManager* aOwner, bool aIsIdleQueue,
+                  uint32_t aMaxIdleDeferMS);
 
   void Shutdown();
 
   nsresult MaybeSchedule(const TimeStamp& aDeadline,
                          const TimeDuration& aMinDelay);
 
   void Cancel();
 
--- a/dom/base/TimeoutManager.cpp
+++ b/dom/base/TimeoutManager.cpp
@@ -17,21 +17,24 @@
 #include "nsITimeoutHandler.h"
 #include "mozilla/dom/DocGroup.h"
 #include "mozilla/dom/PopupBlocker.h"
 #include "mozilla/dom/TabGroup.h"
 #include "TimeoutExecutor.h"
 #include "TimeoutBudgetManager.h"
 #include "mozilla/net/WebSocketEventService.h"
 #include "mozilla/MediaManager.h"
+#ifdef MOZ_GECKO_PROFILER
+#  include "ProfilerMarkerPayload.h"
+#endif
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
-static LazyLogModule gLog("Timeout");
+LazyLogModule gTimeoutLog("Timeout");
 
 static int32_t gRunningTimeoutDepth = 0;
 
 // The default shortest interval/timeout we permit
 #define DEFAULT_MIN_CLAMP_TIMEOUT_VALUE 4                   // 4ms
 #define DEFAULT_MIN_BACKGROUND_TIMEOUT_VALUE 1000           // 1000ms
 #define DEFAULT_MIN_TRACKING_TIMEOUT_VALUE 4                // 4ms
 #define DEFAULT_MIN_TRACKING_BACKGROUND_TIMEOUT_VALUE 1000  // 1000ms
@@ -121,16 +124,75 @@ bool TimeoutManager::IsActive() const {
   // Check if we're playing audio
   if (mWindow.AsInner()->IsPlayingAudio()) {
     return true;
   }
 
   return false;
 }
 
+void TimeoutManager::SetLoading(bool value) {
+  // When moving from loading to non-loading, we may need to
+  // reschedule any existing timeouts from the idle timeout queue
+  // to the normal queue.
+  MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("%p: SetLoading(%d)", this, value));
+  if (mIsLoading && !value) {
+    MoveIdleToActive();
+  }
+  // We don't immediately move existing timeouts to the idle queue if we
+  // move to loading.  When they would have fired, we'll see we're loading
+  // and move them then.
+  mIsLoading = value;
+}
+
+void TimeoutManager::MoveIdleToActive() {
+  uint32_t num = 0;
+  TimeStamp when;
+  TimeStamp now;
+  // Ensure we maintain the ordering of timeouts, so timeouts
+  // never fire before a timeout set for an earlier time, or
+  // before a timeout for the same time already submitted.
+  // See https://html.spec.whatwg.org/#dom-settimeout #16 and #17
+  while (RefPtr<Timeout> timeout = mIdleTimeouts.GetLast()) {
+    if (num == 0) {
+      when = timeout->When();
+    }
+    timeout->remove();
+    mTimeouts.InsertFront(timeout);
+#if MOZ_GECKO_PROFILER
+    if (profiler_is_active()) {
+      if (num == 0) {
+        now = TimeStamp::Now();
+      }
+      TimeDuration elapsed = now - timeout->SubmitTime();
+      TimeDuration target = timeout->When() - timeout->SubmitTime();
+      TimeDuration delta = now - timeout->When();
+      nsPrintfCString marker(
+          "Releasing deferred setTimeout() for %dms (original target time was "
+          "%dms (%dms delta))",
+          int(elapsed.ToMilliseconds()), int(target.ToMilliseconds()),
+          int(delta.ToMilliseconds()));
+      // don't have end before start...
+      profiler_add_marker(
+          "setTimeout release", js::ProfilingStackFrame::Category::DOM,
+          MakeUnique<TextMarkerPayload>(
+              marker, delta.ToMilliseconds() >= 0 ? timeout->When() : now,
+              now));
+    }
+#endif
+    num++;
+  }
+  if (num > 0) {
+    MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(when));
+    mIdleExecutor->Cancel();
+  }
+  MOZ_LOG(gTimeoutLog, LogLevel::Debug,
+          ("%p: Moved %d timeouts from Idle to active", this, num));
+}
+
 uint32_t TimeoutManager::CreateFiringId() {
   uint32_t id = mNextFiringId;
   mNextFiringId += 1;
   if (mNextFiringId == InvalidFiringId) {
     mNextFiringId += 1;
   }
 
   mFiringIdStack.AppendElement(id);
@@ -377,43 +439,49 @@ uint32_t gMaxConsecutiveCallbacksMillise
 
 // Only propagate the open window click permission if the setTimeout() is equal
 // to or less than this value.
 #define DEFAULT_DISABLE_OPEN_CLICK_DELAY 0
 int32_t gDisableOpenClickDelay;
 
 }  // anonymous namespace
 
-TimeoutManager::TimeoutManager(nsGlobalWindowInner& aWindow)
+TimeoutManager::TimeoutManager(nsGlobalWindowInner& aWindow,
+                               uint32_t aMaxIdleDeferMS)
     : mWindow(aWindow),
-      mExecutor(new TimeoutExecutor(this)),
+      mExecutor(new TimeoutExecutor(this, false, 0)),
+      mIdleExecutor(new TimeoutExecutor(this, true, aMaxIdleDeferMS)),
       mTimeouts(*this),
       mTimeoutIdCounter(1),
       mNextFiringId(InvalidFiringId + 1),
       mRunningTimeout(nullptr),
+      mIdleTimeouts(*this),
       mIdleCallbackTimeoutCounter(1),
       mLastBudgetUpdate(TimeStamp::Now()),
       mExecutionBudget(GetMaxBudget(mWindow.IsBackgroundInternal())),
       mThrottleTimeouts(false),
       mThrottleTrackingTimeouts(false),
-      mBudgetThrottleTimeouts(false) {
-  MOZ_LOG(gLog, LogLevel::Debug,
+      mBudgetThrottleTimeouts(false),
+      mIsLoading(false) {
+  MOZ_LOG(gTimeoutLog, LogLevel::Debug,
           ("TimeoutManager %p created, tracking bucketing %s\n", this,
            StaticPrefs::privacy_trackingprotection_annotate_channels()
                ? "enabled"
                : "disabled"));
 }
 
 TimeoutManager::~TimeoutManager() {
   MOZ_DIAGNOSTIC_ASSERT(mWindow.IsDying());
   MOZ_DIAGNOSTIC_ASSERT(!mThrottleTimeoutsTimer);
 
   mExecutor->Shutdown();
+  mIdleExecutor->Shutdown();
 
-  MOZ_LOG(gLog, LogLevel::Debug, ("TimeoutManager %p destroyed\n", this));
+  MOZ_LOG(gTimeoutLog, LogLevel::Debug,
+          ("TimeoutManager %p destroyed\n", this));
 }
 
 /* static */
 void TimeoutManager::Initialize() {
   Preferences::AddIntVarCache(&gMinClampTimeoutValue, "dom.min_timeout_value",
                               DEFAULT_MIN_CLAMP_TIMEOUT_VALUE);
   Preferences::AddIntVarCache(&gMinBackgroundTimeoutValue,
                               "dom.min_background_timeout_value",
@@ -533,54 +601,68 @@ nsresult TimeoutManager::SetTimeout(nsIT
   Timeouts::SortBy sort(mWindow.IsFrozen() ? Timeouts::SortBy::TimeRemaining
                                            : Timeouts::SortBy::TimeWhen);
   mTimeouts.Insert(timeout, sort);
 
   timeout->mTimeoutId = GetTimeoutId(aReason);
   *aReturn = timeout->mTimeoutId;
 
   MOZ_LOG(
-      gLog, LogLevel::Debug,
+      gTimeoutLog, LogLevel::Debug,
       ("Set%s(TimeoutManager=%p, timeout=%p, delay=%i, "
        "minimum=%f, throttling=%s, state=%s(%s), realInterval=%f) "
        "returned timeout ID %u, budget=%d\n",
        aIsInterval ? "Interval" : "Timeout", this, timeout.get(), interval,
        (CalculateDelay(timeout) - timeout->mInterval).ToMilliseconds(),
        mThrottleTimeouts ? "yes" : (mThrottleTimeoutsTimer ? "pending" : "no"),
        IsActive() ? "active" : "inactive",
        mWindow.IsBackgroundInternal() ? "background" : "foreground",
        realInterval.ToMilliseconds(), timeout->mTimeoutId,
        int(mExecutionBudget.ToMilliseconds())));
 
   return NS_OK;
 }
 
+// Make sure we clear it no matter which list it's in
 void TimeoutManager::ClearTimeout(int32_t aTimerId, Timeout::Reason aReason) {
+  if (ClearTimeoutInternal(aTimerId, aReason, false) ||
+      mIdleTimeouts.IsEmpty()) {
+    return;  // no need to check the other list if we cleared the timeout
+  }
+  ClearTimeoutInternal(aTimerId, aReason, true);
+}
+
+bool TimeoutManager::ClearTimeoutInternal(int32_t aTimerId,
+                                          Timeout::Reason aReason,
+                                          bool aIsIdle) {
   uint32_t timerId = (uint32_t)aTimerId;
-
+  Timeouts& timeouts = aIsIdle ? mIdleTimeouts : mTimeouts;
+  RefPtr<TimeoutExecutor>& executor = aIsIdle ? mIdleExecutor : mExecutor;
   bool firstTimeout = true;
   bool deferredDeletion = false;
+  bool cleared = false;
 
-  mTimeouts.ForEachAbortable([&](Timeout* aTimeout) {
-    MOZ_LOG(gLog, LogLevel::Debug,
+  timeouts.ForEachAbortable([&](Timeout* aTimeout) {
+    MOZ_LOG(gTimeoutLog, LogLevel::Debug,
             ("Clear%s(TimeoutManager=%p, timeout=%p, aTimerId=%u, ID=%u)\n",
              aTimeout->mIsInterval ? "Interval" : "Timeout", this, aTimeout,
              timerId, aTimeout->mTimeoutId));
 
     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;
         deferredDeletion = true;
       } else {
         /* Delete the aTimeout from the pending aTimeout list */
         aTimeout->remove();
       }
+      cleared = true;
       return true;  // abort!
     }
 
     firstTimeout = false;
 
     return false;
   });
 
@@ -588,38 +670,47 @@ void TimeoutManager::ClearTimeout(int32_
   //  * If the we weren't cancelling the first timeout, then the executor's
   //    state doesn't need to change.  It will only reflect the next soonest
   //    Timeout.
   //  * If we did cancel the first Timeout, but its currently running, then
   //    RunTimeout() will handle rescheduling the executor.
   //  * If the window has become suspended then we should not start executing
   //    Timeouts.
   if (!firstTimeout || deferredDeletion || mWindow.IsSuspended()) {
-    return;
+    return cleared;
   }
 
   // Stop the executor and restart it at the next soonest deadline.
-  mExecutor->Cancel();
+  executor->Cancel();
 
-  Timeout* nextTimeout = mTimeouts.GetFirst();
+  Timeout* nextTimeout = timeouts.GetFirst();
   if (nextTimeout) {
-    MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextTimeout->When()));
+    if (aIsIdle) {
+      MOZ_ALWAYS_SUCCEEDS(
+          executor->MaybeSchedule(nextTimeout->When(), TimeDuration(0)));
+    } else {
+      MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextTimeout->When()));
+    }
   }
+  return cleared;
 }
 
 void TimeoutManager::RunTimeout(const TimeStamp& aNow,
-                                const TimeStamp& aTargetDeadline) {
+                                const TimeStamp& aTargetDeadline,
+                                bool aProcessIdle) {
   MOZ_DIAGNOSTIC_ASSERT(!aNow.IsNull());
   MOZ_DIAGNOSTIC_ASSERT(!aTargetDeadline.IsNull());
 
   MOZ_ASSERT_IF(mWindow.IsFrozen(), mWindow.IsSuspended());
   if (mWindow.IsSuspended()) {
     return;
   }
 
+  Timeouts& timeouts(aProcessIdle ? mIdleTimeouts : mTimeouts);
+
   // Limit the overall time spent in RunTimeout() to reduce jank.
   uint32_t totalTimeLimitMS =
       std::max(1u, gMaxConsecutiveCallbacksMilliseconds);
   const TimeDuration totalTimeLimit =
       TimeDuration::Min(TimeDuration::FromMilliseconds(totalTimeLimitMS),
                         TimeDuration::Max(TimeDuration(), mExecutionBudget));
 
   // Allow up to 25% of our total time budget to be used figuring out which
@@ -666,17 +757,17 @@ void TimeoutManager::RunTimeout(const Ti
   uint32_t numTimersToRun = 0;
 
   // 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.
-  for (Timeout* timeout = mTimeouts.GetFirst(); timeout != nullptr;
+  for (Timeout* timeout = timeouts.GetFirst(); timeout != nullptr;
        timeout = timeout->getNext()) {
     if (totalTimeLimit.IsZero() || timeout->When() > deadline) {
       nextDeadline = timeout->When();
       break;
     }
 
     if (IsInvalidFiringId(timeout->mFiringId)) {
       // Mark any timeouts that are on the list to be fired with the
@@ -691,30 +782,44 @@ void TimeoutManager::RunTimeout(const Ti
         TimeDuration elapsed(now - start);
         if (elapsed >= initialTimeLimit) {
           nextDeadline = timeout->When();
           break;
         }
       }
     }
   }
+  if (aProcessIdle) {
+    MOZ_LOG(gTimeoutLog, LogLevel::Debug,
+            ("Running %u deferred timeouts on idle (TimeoutManager=%p), "
+             "nextDeadline = %gms from now",
+             numTimersToRun, this, (nextDeadline - now).ToMilliseconds()));
+  }
 
   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()) {
     // Note, we verified the window is not suspended at the top of
     // method and the window should not have been suspended while
     // executing the loop above since it doesn't call out to js.
     MOZ_DIAGNOSTIC_ASSERT(!mWindow.IsSuspended());
-    MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextDeadline, now));
+    if (aProcessIdle) {
+      // We don't want to update timing budget for idle queue firings, and
+      // all timeouts in the IdleTimeouts list have hit their deadlines,
+      // and so should run as soon as possible.
+      MOZ_ALWAYS_SUCCEEDS(
+          mIdleExecutor->MaybeSchedule(nextDeadline, TimeDuration()));
+    } else {
+      MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextDeadline, now));
+    }
   }
 
   // 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 (!numTimersToRun) {
     return;
   }
@@ -731,26 +836,31 @@ void TimeoutManager::RunTimeout(const Ti
     // iterating are freed after the loop.
 
     // The next timeout to run. This is used to advance the loop, but
     // we cannot set it until we've run the current timeout, since
     // running the current timeout might remove the immediate next
     // timeout.
     RefPtr<Timeout> next;
 
-    for (RefPtr<Timeout> timeout = mTimeouts.GetFirst(); timeout != nullptr;
+    for (RefPtr<Timeout> timeout = timeouts.GetFirst(); timeout != nullptr;
          timeout = next) {
       next = timeout->getNext();
       // We should only execute callbacks for the set of expired Timeout
       // objects we computed above.
       if (timeout->mFiringId != firingId) {
         // If the FiringId does not match, but is still valid, then this is
-        // a TImeout for another RunTimeout() on the call stack.  Just
+        // a Timeout for another RunTimeout() on the call stack.  Just
         // skip it.
         if (IsValidFiringId(timeout->mFiringId)) {
+          MOZ_LOG(gTimeoutLog, LogLevel::Debug,
+                  ("Skipping Run%s(TimeoutManager=%p, timeout=%p) since "
+                   "firingId %d is valid (processing firingId %d)",
+                   timeout->mIsInterval ? "Interval" : "Timeout", this,
+                   timeout.get(), timeout->mFiringId, firingId));
           continue;
         }
 
         // If, however, the FiringId is invalid then we have reached Timeout
         // objects beyond the list we calculated above.  This can happen
         // if the Timeout just beyond our last expired Timeout is cancelled
         // by one of the callbacks we've just executed.  In this case we
         // should just stop iterating.  We're done.
@@ -762,90 +872,144 @@ void TimeoutManager::RunTimeout(const Ti
       MOZ_ASSERT_IF(mWindow.IsFrozen(), mWindow.IsSuspended());
       if (mWindow.IsSuspended()) {
         break;
       }
 
       // The timeout is on the list to run at this depth, go ahead and
       // process it.
 
-      // Get the script context (a strong ref to prevent it going away)
-      // for this timeout and ensure the script language is enabled.
-      nsCOMPtr<nsIScriptContext> scx = mWindow.GetContextInternal();
-
-      if (!scx) {
-        // No context means this window was closed or never properly
-        // initialized for this language.  This timer will never fire
-        // so just remove it.
+      if (mIsLoading && !aProcessIdle) {
+        // Any timeouts that would fire during a load will be deferred
+        // until the load event occurs, but if there's an idle time,
+        // they'll be run before the load event.
         timeout->remove();
-        continue;
-      }
+        // MOZ_RELEASE_ASSERT(timeout->When() <= (TimeStamp::Now()));
+        mIdleTimeouts.InsertBack(timeout);
+        if (MOZ_LOG_TEST(gTimeoutLog, LogLevel::Debug)) {
+          uint32_t num = 0;
+          for (Timeout* t = mIdleTimeouts.GetFirst(); t != nullptr;
+               t = t->getNext()) {
+            num++;
+          }
+          MOZ_LOG(
+              gTimeoutLog, LogLevel::Debug,
+              ("Deferring Run%s(TimeoutManager=%p, timeout=%p (%gms in the "
+               "past)) (%u deferred)",
+               timeout->mIsInterval ? "Interval" : "Timeout", this,
+               timeout.get(), (now - timeout->When()).ToMilliseconds(), num));
+        }
+        MOZ_ALWAYS_SUCCEEDS(mIdleExecutor->MaybeSchedule(now, TimeDuration()));
+      } else {
+        // Get the script context (a strong ref to prevent it going away)
+        // for this timeout and ensure the script language is enabled.
+        nsCOMPtr<nsIScriptContext> scx = mWindow.GetContextInternal();
 
-      // This timeout is good to run
-      bool timeout_was_cleared = mWindow.RunTimeoutHandler(timeout, scx);
-
-      MOZ_LOG(gLog, LogLevel::Debug,
-              ("Run%s(TimeoutManager=%p, timeout=%p) returned %d\n",
-               timeout->mIsInterval ? "Interval" : "Timeout", this,
-               timeout.get(), !!timeout_was_cleared));
-
-      if (timeout_was_cleared) {
-        // Make sure we're not holding any Timeout objects alive.
-        next = nullptr;
-
-        // Since ClearAllTimeouts() was called the lists should be empty.
-        MOZ_DIAGNOSTIC_ASSERT(!HasTimeouts());
+        if (!scx) {
+          // No context means this window was closed or never properly
+          // initialized for this language.  This timer will never fire
+          // so just remove it.
+          timeout->remove();
+          continue;
+        }
 
-        return;
-      }
+        // This timeout is good to run
+#if MOZ_GECKO_PROFILER
+        if (profiler_is_active()) {
+          if (aProcessIdle) {
+            TimeDuration elapsed = now - timeout->SubmitTime();
+            TimeDuration target = timeout->When() - timeout->SubmitTime();
+            TimeDuration delta = now - timeout->When();
+            nsPrintfCString marker(
+                "%ssetTimeout() for %dms (original target time was %dms (%dms "
+                "delta))",
+                aProcessIdle ? "Deferred " : "", int(elapsed.ToMilliseconds()),
+                int(target.ToMilliseconds()), int(delta.ToMilliseconds()));
+            // don't have end before start...
+            profiler_add_marker(
+                "setTimeout", js::ProfilingStackFrame::Category::DOM,
+                MakeUnique<TextMarkerPayload>(
+                    marker, delta.ToMilliseconds() >= 0 ? timeout->When() : now,
+                    now));
+          }
+        }
+#endif
+        bool timeout_was_cleared = mWindow.RunTimeoutHandler(timeout, scx);
 
-      // If we need to reschedule a setInterval() the delay should be
-      // calculated based on when its callback started to execute.  So
-      // save off the last time before updating our "now" timestamp to
-      // account for its callback execution time.
-      TimeStamp lastCallbackTime = now;
-      now = TimeStamp::Now();
+        MOZ_LOG(gTimeoutLog, LogLevel::Debug,
+                ("Run%s(TimeoutManager=%p, timeout=%p) returned %d\n",
+                 timeout->mIsInterval ? "Interval" : "Timeout", this,
+                 timeout.get(), !!timeout_was_cleared));
 
-      // If we have a regular interval timer, we re-schedule the
-      // timeout, accounting for clock drift.
-      bool needsReinsertion = RescheduleTimeout(timeout, lastCallbackTime, now);
+        if (timeout_was_cleared) {
+          // Make sure we're not holding any Timeout objects alive.
+          next = nullptr;
 
-      // Running a timeout can cause another timeout to be deleted, so
-      // we need to reset the pointer to the following timeout.
-      next = timeout->getNext();
+          // Since ClearAllTimeouts() was called the lists should be empty.
+          MOZ_DIAGNOSTIC_ASSERT(!HasTimeouts());
+
+          return;
+        }
 
-      timeout->remove();
+        // If we need to reschedule a setInterval() the delay should be
+        // calculated based on when its callback started to execute.  So
+        // save off the last time before updating our "now" timestamp to
+        // account for its callback execution time.
+        TimeStamp lastCallbackTime = now;
+        now = TimeStamp::Now();
 
-      if (needsReinsertion) {
-        // Insert interval timeout onto the corresponding list sorted in
-        // deadline order. AddRefs timeout.
-        mTimeouts.Insert(timeout, mWindow.IsFrozen()
-                                      ? Timeouts::SortBy::TimeRemaining
-                                      : Timeouts::SortBy::TimeWhen);
+        // If we have a regular interval timer, we re-schedule the
+        // timeout, accounting for clock drift.
+        bool needsReinsertion =
+            RescheduleTimeout(timeout, lastCallbackTime, now);
+
+        // Running a timeout can cause another timeout to be deleted, so
+        // we need to reset the pointer to the following timeout.
+        next = timeout->getNext();
+
+        timeout->remove();
+
+        if (needsReinsertion) {
+          // Insert interval timeout onto the corresponding list sorted in
+          // deadline order. AddRefs timeout.
+          // Always re-insert into the normal time queue!
+          mTimeouts.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 = 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.  Its possible,
         // however, that the last timeout handler suspended the window.  If
         // that happened then we must skip this step.
         if (!mWindow.IsSuspended()) {
           if (next) {
-            // If we ran out of execution budget we need to force a
-            // reschedule. By cancelling the executor we will not run
-            // immediately, but instead reschedule to the minimum
-            // scheduling delay.
-            if (mExecutionBudget < TimeDuration()) {
-              mExecutor->Cancel();
+            if (aProcessIdle) {
+              // We don't want to update timing budget for idle queue firings,
+              // and all timeouts in the IdleTimeouts list have hit their
+              // deadlines, and so should run as soon as possible.
+
+              // Shouldn't need cancelling since it never waits
+              MOZ_ALWAYS_SUCCEEDS(
+                  mIdleExecutor->MaybeSchedule(next->When(), TimeDuration()));
+            } else {
+              // If we ran out of execution budget we need to force a
+              // reschedule. By cancelling the executor we will not run
+              // immediately, but instead reschedule to the minimum
+              // scheduling delay.
+              if (mExecutionBudget < TimeDuration()) {
+                mExecutor->Cancel();
+              }
+
+              MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(next->When(), now));
             }
-
-            MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(next->When(), now));
           }
         }
         break;
       }
     }
   }
 }
 
@@ -888,43 +1052,45 @@ bool TimeoutManager::RescheduleTimeout(T
   NS_ENSURE_SUCCESS(rv, false);
 
   return true;
 }
 
 void TimeoutManager::ClearAllTimeouts() {
   bool seenRunningTimeout = false;
 
-  MOZ_LOG(gLog, LogLevel::Debug,
+  MOZ_LOG(gTimeoutLog, LogLevel::Debug,
           ("ClearAllTimeouts(TimeoutManager=%p)\n", this));
 
   if (mThrottleTimeoutsTimer) {
     mThrottleTimeoutsTimer->Cancel();
     mThrottleTimeoutsTimer = nullptr;
   }
 
   mExecutor->Cancel();
+  mIdleExecutor->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;
     }
 
     // Set timeout->mCleared to true to indicate that the timeout was
     // cleared and taken out of the list of timeouts
     aTimeout->mCleared = true;
   });
 
-  // Clear out our list
+  // Clear out our lists
   mTimeouts.Clear();
+  mIdleTimeouts.Clear();
 }
 
 void TimeoutManager::Timeouts::Insert(Timeout* aTimeout, SortBy aSortBy) {
   // Start at mLastTimeout and go backwards.  Stop if we see a Timeout with a
   // valid FiringId since those timers are currently being processed by
   // RunTimeout.  This optimizes for the common case of insertion at the end.
   Timeout* prevSibling;
   for (prevSibling = GetLast();
@@ -971,44 +1137,50 @@ void TimeoutManager::UnmarkGrayTimers() 
   ForEachUnorderedTimeout([](Timeout* aTimeout) {
     if (aTimeout->mScriptHandler) {
       aTimeout->mScriptHandler->MarkForCC();
     }
   });
 }
 
 void TimeoutManager::Suspend() {
-  MOZ_LOG(gLog, LogLevel::Debug, ("Suspend(TimeoutManager=%p)\n", this));
+  MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Suspend(TimeoutManager=%p)\n", this));
 
   if (mThrottleTimeoutsTimer) {
     mThrottleTimeoutsTimer->Cancel();
     mThrottleTimeoutsTimer = nullptr;
   }
 
   mExecutor->Cancel();
+  mIdleExecutor->Cancel();
 }
 
 void TimeoutManager::Resume() {
-  MOZ_LOG(gLog, LogLevel::Debug, ("Resume(TimeoutManager=%p)\n", this));
+  MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Resume(TimeoutManager=%p)\n", this));
 
   // When Suspend() has been called after IsDocumentLoaded(), but the
   // throttle tracking timer never managed to fire, start the timer
   // again.
   if (mWindow.AsInner()->IsDocumentLoaded() && !mThrottleTimeouts) {
     MaybeStartThrottleTimeout();
   }
 
   Timeout* nextTimeout = mTimeouts.GetFirst();
   if (nextTimeout) {
     MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextTimeout->When()));
   }
+  nextTimeout = mIdleTimeouts.GetFirst();
+  if (nextTimeout) {
+    MOZ_ALWAYS_SUCCEEDS(
+        mIdleExecutor->MaybeSchedule(nextTimeout->When(), TimeDuration()));
+  }
 }
 
 void TimeoutManager::Freeze() {
-  MOZ_LOG(gLog, LogLevel::Debug, ("Freeze(TimeoutManager=%p)\n", this));
+  MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Freeze(TimeoutManager=%p)\n", this));
 
   TimeStamp now = TimeStamp::Now();
   ForEachUnorderedTimeout([&](Timeout* aTimeout) {
     // Save the current remaining time for this timeout.  We will
     // re-apply it when the window is Thaw()'d.  This effectively
     // shifts timers to the right as if time does not pass while
     // the window is frozen.
     TimeDuration delta(0);
@@ -1016,17 +1188,17 @@ void TimeoutManager::Freeze() {
       delta = aTimeout->When() - now;
     }
     aTimeout->SetWhenOrTimeRemaining(now, delta);
     MOZ_DIAGNOSTIC_ASSERT(aTimeout->TimeRemaining() == delta);
   });
 }
 
 void TimeoutManager::Thaw() {
-  MOZ_LOG(gLog, LogLevel::Debug, ("Thaw(TimeoutManager=%p)\n", this));
+  MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Thaw(TimeoutManager=%p)\n", this));
 
   TimeStamp now = TimeStamp::Now();
 
   ForEachUnorderedTimeout([&](Timeout* aTimeout) {
     // Set When() back to the time when the timer is supposed to fire.
     aTimeout->SetWhenOrTimeRemaining(now, aTimeout->TimeRemaining());
     MOZ_DIAGNOSTIC_ASSERT(!aTimeout->When().IsNull());
   });
@@ -1040,16 +1212,27 @@ void TimeoutManager::UpdateBackgroundSta
   // changed.  Only do this if the window is not suspended and we
   // actually have a timeout.
   if (!mWindow.IsSuspended()) {
     Timeout* nextTimeout = mTimeouts.GetFirst();
     if (nextTimeout) {
       mExecutor->Cancel();
       MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextTimeout->When()));
     }
+    // the Idle queue should all be past their firing time, so there we just
+    // need to restart the queue
+
+    // XXX May not be needed if we don't stop the idle queue, as
+    // MinSchedulingDelay isn't relevant here
+    nextTimeout = mIdleTimeouts.GetFirst();
+    if (nextTimeout) {
+      mIdleExecutor->Cancel();
+      MOZ_ALWAYS_SUCCEEDS(
+          mIdleExecutor->MaybeSchedule(nextTimeout->When(), TimeDuration()));
+    }
   }
 }
 
 namespace {
 
 class ThrottleTimeoutsCallback final : public nsITimerCallback,
                                        public nsINamed {
  public:
@@ -1119,17 +1302,17 @@ bool TimeoutManager::BudgetThrottlingEna
 
   return true;
 }
 
 void TimeoutManager::StartThrottlingTimeouts() {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_DIAGNOSTIC_ASSERT(mThrottleTimeoutsTimer);
 
-  MOZ_LOG(gLog, LogLevel::Debug,
+  MOZ_LOG(gTimeoutLog, LogLevel::Debug,
           ("TimeoutManager %p started to throttle tracking timeouts\n", this));
 
   MOZ_DIAGNOSTIC_ASSERT(!mThrottleTimeouts);
   mThrottleTimeouts = true;
   mThrottleTrackingTimeouts = true;
   mBudgetThrottleTimeouts = gEnableBudgetTimeoutThrottling;
   mThrottleTimeoutsTimer = nullptr;
 }
@@ -1146,17 +1329,17 @@ void TimeoutManager::OnDocumentLoaded() 
 void TimeoutManager::MaybeStartThrottleTimeout() {
   if (gTimeoutThrottlingDelay <= 0 || mWindow.IsDying() ||
       mWindow.IsSuspended()) {
     return;
   }
 
   MOZ_DIAGNOSTIC_ASSERT(!mThrottleTimeouts);
 
-  MOZ_LOG(gLog, LogLevel::Debug,
+  MOZ_LOG(gTimeoutLog, LogLevel::Debug,
           ("TimeoutManager %p delaying tracking timeout throttling by %dms\n",
            this, gTimeoutThrottlingDelay));
 
   nsCOMPtr<nsITimerCallback> callback = new ThrottleTimeoutsCallback(&mWindow);
 
   NS_NewTimerWithCallback(getter_AddRefs(mThrottleTimeoutsTimer), callback,
                           gTimeoutThrottlingDelay, nsITimer::TYPE_ONE_SHOT,
                           EventTarget());
--- a/dom/base/TimeoutManager.h
+++ b/dom/base/TimeoutManager.h
@@ -20,36 +20,45 @@ namespace mozilla {
 class PerformanceCounter;
 
 namespace dom {
 
 class TimeoutExecutor;
 
 // This class manages the timeouts in a Window's setTimeout/setInterval pool.
 class TimeoutManager final {
+ private:
+  struct Timeouts;
+
  public:
-  explicit TimeoutManager(nsGlobalWindowInner& aWindow);
+  TimeoutManager(nsGlobalWindowInner& aWindow, uint32_t aMaxIdleDeferMS);
   ~TimeoutManager();
   TimeoutManager(const TimeoutManager& rhs) = delete;
   void operator=(const TimeoutManager& rhs) = delete;
 
   bool IsRunningTimeout() const;
 
   static uint32_t GetNestingLevel() { return sNestingLevel; }
   static void SetNestingLevel(uint32_t aLevel) { sNestingLevel = aLevel; }
 
-  bool HasTimeouts() const { return !mTimeouts.IsEmpty(); }
+  bool HasTimeouts() const {
+    return !mTimeouts.IsEmpty() || !mIdleTimeouts.IsEmpty();
+  }
 
   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);
+  bool ClearTimeoutInternal(int32_t aTimerId,
+                            mozilla::dom::Timeout::Reason aReason,
+                            bool aIsIdle);
 
   // The timeout implementation functions.
-  void RunTimeout(const TimeStamp& aNow, const TimeStamp& aTargetDeadline);
+  void RunTimeout(const TimeStamp& aNow, const TimeStamp& aTargetDeadline,
+                  bool aProcessIdle);
 
   void ClearAllTimeouts();
   uint32_t GetTimeoutId(mozilla::dom::Timeout::Reason aReason);
 
   TimeDuration CalculateDelay(Timeout* aTimeout) const;
 
   // aTimeout is the timeout that we're about to start running.  This function
   // returns the current timeout.
@@ -76,36 +85,41 @@ class TimeoutManager final {
   // The document finished loading
   void OnDocumentLoaded();
   void StartThrottlingTimeouts();
 
   // Run some code for each Timeout in our list.  Note that this function
   // doesn't guarantee that Timeouts are iterated in any particular order.
   template <class Callable>
   void ForEachUnorderedTimeout(Callable c) {
+    mIdleTimeouts.ForEach(c);
     mTimeouts.ForEach(c);
   }
 
   void BeginSyncOperation();
   void EndSyncOperation();
 
   nsIEventTarget* EventTarget();
 
   bool BudgetThrottlingEnabled(bool aIsBackground) const;
 
   static const uint32_t InvalidFiringId;
 
+  void SetLoading(bool value);
+
  private:
   void MaybeStartThrottleTimeout();
 
   // Return true if |aTimeout| needs to be reinserted into the timeout list.
   bool RescheduleTimeout(mozilla::dom::Timeout* aTimeout,
                          const TimeStamp& aLastCallbackTime,
                          const TimeStamp& aCurrentNow);
 
+  void MoveIdleToActive();
+
   bool IsBackground() const;
 
   bool IsActive() const;
 
   uint32_t CreateFiringId();
 
   void DestroyFiringId(uint32_t aFiringId);
 
@@ -136,16 +150,17 @@ class TimeoutManager final {
     void Insert(mozilla::dom::Timeout* aTimeout, 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 InsertBack(Timeout* aTimeout) { mTimeoutList.insertBack(aTimeout); }
     void Clear() { mTimeoutList.clear(); }
 
     template <class Callable>
     void ForEach(Callable c) {
       for (Timeout* timeout = GetFirst(); timeout;
            timeout = timeout->getNext()) {
         c(timeout);
       }
@@ -177,33 +192,41 @@ class TimeoutManager final {
 
   // Each nsGlobalWindowInner object has a TimeoutManager member.  This
   // reference points to that holder object.
   nsGlobalWindowInner& 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;
+  // For timeouts run off the idle queue
+  RefPtr<TimeoutExecutor> mIdleExecutor;
   // The list of timeouts coming from non-tracking scripts.
   Timeouts mTimeouts;
   uint32_t mTimeoutIdCounter;
   uint32_t mNextFiringId;
   AutoTArray<uint32_t, 2> mFiringIdStack;
   mozilla::dom::Timeout* mRunningTimeout;
 
+  // Timeouts that would have fired but are being deferred until MainThread
+  // is idle (because we're loading)
+  Timeouts mIdleTimeouts;
+
   // The current idle request callback timeout handle
   uint32_t mIdleCallbackTimeoutCounter;
 
   nsCOMPtr<nsITimer> mThrottleTimeoutsTimer;
   mozilla::TimeStamp mLastBudgetUpdate;
   mozilla::TimeDuration mExecutionBudget;
 
   bool mThrottleTimeouts;
   bool mThrottleTrackingTimeouts;
   bool mBudgetThrottleTimeouts;
 
+  bool mIsLoading;
+
   static uint32_t sNestingLevel;
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -881,17 +881,18 @@ nsGlobalWindowInner::nsGlobalWindowInner
   nsLayoutStatics::AddRef();
 
   // Initialize the PRCList (this).
   PR_INIT_CLIST(this);
 
   // add this inner window to the outer window list of inners.
   PR_INSERT_AFTER(this, aOuterWindow);
 
-  mTimeoutManager = MakeUnique<dom::TimeoutManager>(*this);
+  mTimeoutManager = MakeUnique<dom::TimeoutManager>(
+      *this, StaticPrefs::dom_timeout_max_idle_defer_ms());
 
   mObserver = new nsGlobalWindowObserver(this);
   nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
   if (os) {
     // Watch for online/offline status changes so we can fire events. Use
     // a strong reference.
     os->AddObserver(mObserver, NS_IOSERVICE_OFFLINE_STATUS_TOPIC, false);
 
@@ -2536,16 +2537,22 @@ bool nsPIDOMWindowInner::HasOpenWebSocke
 
 void nsPIDOMWindowInner::SetAudioCapture(bool aCapture) {
   RefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate();
   if (service) {
     service->SetWindowAudioCaptured(GetOuterWindow(), mWindowID, aCapture);
   }
 }
 
+void nsPIDOMWindowInner::SetActiveLoadingState(bool aIsLoading) /* const? */ {
+  if (!nsGlobalWindowInner::Cast(this)->IsChromeWindow()) {
+    mTimeoutManager->SetLoading(aIsLoading);
+  }
+}
+
 // nsISpeechSynthesisGetter
 
 #ifdef MOZ_WEBSPEECH
 SpeechSynthesis* nsGlobalWindowInner::GetSpeechSynthesis(ErrorResult& aError) {
   if (!mSpeechSynthesis) {
     mSpeechSynthesis = new SpeechSynthesis(this);
   }
 
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -2051,16 +2051,22 @@ nsresult nsGlobalWindowOuter::SetNewDocu
       xpc::ClearContentXBLScope(newInnerGlobal);
     }
   } else {
     if (aState) {
       newInnerWindow = wsh->GetInnerWindow();
       newInnerGlobal = newInnerWindow->GetWrapperPreserveColor();
     } else {
       newInnerWindow = nsGlobalWindowInner::Create(this, thisChrome);
+      if (StaticPrefs::dom_timeout_defer_during_load()) {
+        // ensure the initial loading state is known
+        newInnerWindow->SetActiveLoadingState(
+            aDocument->GetReadyStateEnum() ==
+            Document::ReadyState::READYSTATE_LOADING);
+      }
 
       // The outer window is automatically treated as frozen when we
       // null out the inner window. As a result, initializing classes
       // on the new inner won't end up reaching into the old inner
       // window for classes etc.
       //
       // [This happens with Object.prototype when XPConnect creates
       // a temporary global while initializing classes; the reason
--- a/dom/base/nsPIDOMWindow.h
+++ b/dom/base/nsPIDOMWindow.h
@@ -164,20 +164,24 @@ class nsPIDOMWindowInner : public mozIDO
   // Returns true if the document of this window is the active document.  This
   // is not identical to IsCurrentInnerWindow() because document.open() will
   // keep the same document active but create a new window.
   inline bool HasActiveDocument();
 
   // Returns true if this window is the same as mTopInnerWindow
   inline bool IsTopInnerWindow() const;
 
-  // Check whether a document is currently loading
+  // Check whether a document is currently loading (really checks if the
+  // load event has completed).  May not be reset to false on errors.
   inline bool IsLoading() const;
   inline bool IsHandlingResizeEvent() const;
 
+  // Note: not related to IsLoading.  Set to false if there's an error, etc.
+  void SetActiveLoadingState(bool aIsActiveLoading);
+
   bool AddAudioContext(mozilla::dom::AudioContext* aAudioContext);
   void RemoveAudioContext(mozilla::dom::AudioContext* aAudioContext);
   void MuteAudioContexts();
   void UnmuteAudioContexts();
 
   void SetAudioCapture(bool aCapture);
 
   mozilla::dom::Performance* GetPerformance();
--- a/modules/libpref/init/StaticPrefList.h
+++ b/modules/libpref/init/StaticPrefList.h
@@ -239,16 +239,31 @@ VARCACHE_PREF(
 )
 
 VARCACHE_PREF(
   "dom.performance.enable_scheduler_timing",
   dom_performance_enable_scheduler_timing,
   RelaxedAtomicBool, true
 )
 
+// Should we defer timeouts and intervals while loading a page.  Released
+// on Idle or when the page is loaded.
+VARCACHE_PREF(
+  "dom.timeout.defer_during_load",
+  dom_timeout_defer_during_load,
+  bool, true
+)
+
+// Maximum deferral time for setTimeout/Interval in milliseconds
+VARCACHE_PREF(
+  "dom.timeout.max_idle_defer_ms",
+  dom_timeout_max_idle_defer_ms,
+  uint32_t, 10*1000
+)
+
 VARCACHE_PREF(
   "dom.performance.children_results_ipc_timeout",
   dom_performance_children_results_ipc_timeout,
   uint32_t, 1000
 )
 
 // If true. then the service worker interception and the ServiceWorkerManager
 // will live in the parent process.  This only takes effect on browser start.