Backed out 13 changesets (bug 1703443) for causing GTest failures.
authorBrindusan Cristian <cbrindusan@mozilla.com>
Fri, 25 Jun 2021 10:22:03 +0300
changeset 584362 531323de1a48ea8a49a329fb22f08373e46df620
parent 584361 74ceabefad3f03602626af9fae26cbd7d83446fe
child 584363 634121e78fb63a84a8cc721db053a4ca1135b2c9
child 584364 7f4be4c5eb8093e62a7c891c798b82c5edb8918d
push id38563
push usercbrindusan@mozilla.com
push dateFri, 25 Jun 2021 09:34:36 +0000
treeherdermozilla-central@531323de1a48 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1703443
milestone91.0a1
backs out5b2b19d5a0a20b73e26bd7912a6fe610134095ae
589311f587cec9b4950a9e7cc5d1a685ccadc2dd
e8c6a26493b411e540d81f75d1cd518ce167f18e
c602e39f64fd559efff13d0a20b7da7e94b05ff5
8e6c4e4c5429ebd45f9d878c9bceca04a4982750
f55e0c5e710fa1021cf351e036a089dfc95a193b
251a9a597c911479390988ac16637dbf1d53ce16
341dd4d5b3706c9ae84e072ccc044ea68ce5a268
11ccf870e64384b38f7de108133a50769a7269e2
39bd5b2ec9e041c88d64a144e50a4ed4ace7d4ea
5a900829cbd1ca5b386540fa8dae20ca11559a8f
7e2c90da4fbbdf30e031f49e6201ca98e2c09f91
742f1dc1612fc346aa39488bdbff3f2cc897edcd
first release with
nightly linux32
531323de1a48 / 91.0a1 / 20210625093436 / files
nightly linux64
531323de1a48 / 91.0a1 / 20210625093436 / files
nightly mac
531323de1a48 / 91.0a1 / 20210625093436 / files
nightly win32
531323de1a48 / 91.0a1 / 20210625093436 / files
nightly win64
531323de1a48 / 91.0a1 / 20210625093436 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out 13 changesets (bug 1703443) for causing GTest failures. CLOSED TREE Backed out changeset 5b2b19d5a0a2 (bug 1703443) Backed out changeset 589311f587ce (bug 1703443) Backed out changeset e8c6a26493b4 (bug 1703443) Backed out changeset c602e39f64fd (bug 1703443) Backed out changeset 8e6c4e4c5429 (bug 1703443) Backed out changeset f55e0c5e710f (bug 1703443) Backed out changeset 251a9a597c91 (bug 1703443) Backed out changeset 341dd4d5b370 (bug 1703443) Backed out changeset 11ccf870e643 (bug 1703443) Backed out changeset 39bd5b2ec9e0 (bug 1703443) Backed out changeset 5a900829cbd1 (bug 1703443) Backed out changeset 7e2c90da4fbb (bug 1703443) Backed out changeset 742f1dc1612f (bug 1703443)
dom/base/CCGCScheduler.cpp
dom/base/CCGCScheduler.h
dom/base/moz.build
dom/base/nsJSEnvironment.cpp
dom/base/nsJSEnvironment.h
deleted file mode 100644
--- a/dom/base/CCGCScheduler.cpp
+++ /dev/null
@@ -1,680 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-#include "CCGCScheduler.h"
-
-#include "mozilla/StaticPrefs_javascript.h"
-#include "mozilla/CycleCollectedJSRuntime.h"
-#include "mozilla/dom/ScriptSettings.h"
-
-namespace mozilla {
-
-void CCGCScheduler::FullGCTimerFired(nsITimer* aTimer) {
-  KillFullGCTimer();
-
-  RefPtr<CCGCScheduler::MayGCPromise> mbPromise =
-      CCGCScheduler::MayGCNow(JS::GCReason::FULL_GC_TIMER);
-  if (mbPromise) {
-    mbPromise->Then(
-        GetMainThreadSerialEventTarget(), __func__,
-        [](bool aIgnored) {
-          nsJSContext::GarbageCollectNow(JS::GCReason::FULL_GC_TIMER,
-                                         nsJSContext::IncrementalGC);
-        },
-        [](mozilla::ipc::ResponseRejectReason r) {
-          // do nothing
-        });
-  }
-}
-
-// nsJSEnvironmentObserver observes the user-interaction-inactive notifications
-// and triggers a shrinking a garbage collection if the user is still inactive
-// after NS_SHRINKING_GC_DELAY ms later, if the appropriate pref is set.
-
-void CCGCScheduler::ShrinkingGCTimerFired(nsITimer* aTimer) {
-  KillShrinkingGCTimer();
-
-  RefPtr<MayGCPromise> mbPromise = MayGCNow(JS::GCReason::USER_INACTIVE);
-  if (mbPromise) {
-    mbPromise->Then(
-        GetMainThreadSerialEventTarget(), __func__,
-        [this](bool aIgnored) {
-          if (!mUserIsActive) {
-            mIsCompactingOnUserInactive = true;
-            nsJSContext::GarbageCollectNow(JS::GCReason::USER_INACTIVE,
-                                           nsJSContext::IncrementalGC,
-                                           nsJSContext::ShrinkingGC);
-          } else {
-            using mozilla::ipc::IdleSchedulerChild;
-            IdleSchedulerChild* child =
-                IdleSchedulerChild::GetMainThreadIdleScheduler();
-            if (child) {
-              child->DoneGC();
-            }
-          }
-        },
-        [](mozilla::ipc::ResponseRejectReason r) {});
-  }
-}
-
-bool CCGCScheduler::GCRunnerFired(TimeStamp aDeadline) {
-  MOZ_ASSERT(!mDidShutdown, "GCRunner still alive during shutdown");
-
-  GCRunnerStep step = GetNextGCRunnerAction(aDeadline);
-  switch (step.mAction) {
-    case GCRunnerAction::None:
-      MOZ_CRASH("Unexpected GCRunnerAction");
-
-    case GCRunnerAction::WaitToMajorGC: {
-      RefPtr<CCGCScheduler::MayGCPromise> mbPromise =
-          CCGCScheduler::MayGCNow(step.mReason);
-      if (!mbPromise || mbPromise->IsResolved()) {
-        // Only use the promise if it's not resolved yet, otherwise fall through
-        // and begin the GC in the current idle time with our current deadline.
-        break;
-      }
-
-      KillGCRunner();
-      mbPromise->Then(
-          GetMainThreadSerialEventTarget(), __func__,
-          [this](bool aIgnored) {
-            if (!NoteReadyForMajorGC()) {
-              return;  // Another GC completed while waiting.
-            }
-            // If a new runner was started, recreate it with a 0 delay. The new
-            // runner will continue in idle time.
-            KillGCRunner();
-            EnsureGCRunner(0);
-          },
-          [](mozilla::ipc::ResponseRejectReason r) {});
-
-      return true;
-    }
-
-    case GCRunnerAction::StartMajorGC:
-    case GCRunnerAction::GCSlice:
-      break;
-  }
-
-  // Run a GC slice, possibly the first one of a major GC.
-
-  MOZ_ASSERT(mActiveIntersliceGCBudget);
-  TimeStamp startTimeStamp = TimeStamp::Now();
-  TimeDuration budget = ComputeInterSliceGCBudget(aDeadline, startTimeStamp);
-  TimeDuration duration = mGCUnnotifiedTotalTime;
-  nsJSContext::GarbageCollectNow(step.mReason, nsJSContext::IncrementalGC,
-                                 nsJSContext::NonShrinkingGC,
-                                 budget.ToMilliseconds());
-
-  mGCUnnotifiedTotalTime = TimeDuration();
-  TimeStamp now = TimeStamp::Now();
-  TimeDuration sliceDuration = now - startTimeStamp;
-  duration += sliceDuration;
-  if (duration.ToSeconds()) {
-    TimeDuration idleDuration;
-    if (!aDeadline.IsNull()) {
-      if (aDeadline < now) {
-        // This slice overflowed the idle period.
-        idleDuration = aDeadline - startTimeStamp;
-      } else {
-        // Note, we don't want to use duration here, since it may contain
-        // data also from JS engine triggered GC slices.
-        idleDuration = sliceDuration;
-      }
-    }
-
-    uint32_t percent =
-        uint32_t(idleDuration.ToSeconds() / duration.ToSeconds() * 100);
-    Telemetry::Accumulate(Telemetry::GC_SLICE_DURING_IDLE, percent);
-  }
-
-  // If the GC doesn't have any more work to do on the foreground thread (and
-  // e.g. is waiting for background sweeping to finish) then return false to
-  // make IdleTaskRunner postpone the next call a bit.
-  JSContext* cx = dom::danger::GetJSContext();
-  return JS::IncrementalGCHasForegroundWork(cx);
-}
-
-RefPtr<CCGCScheduler::MayGCPromise> CCGCScheduler::MayGCNow(
-    JS::GCReason reason) {
-  using namespace mozilla::ipc;
-
-  // We ask the parent if we should GC for GCs that aren't too timely,
-  // with the exception of MEM_PRESSURE, in that case we ask the parent
-  // because GCing on too many processes at the same time when under
-  // memory pressure could be a very bad experience for the user.
-  switch (reason) {
-    case JS::GCReason::PAGE_HIDE:
-    case JS::GCReason::MEM_PRESSURE:
-    case JS::GCReason::USER_INACTIVE:
-    case JS::GCReason::FULL_GC_TIMER:
-    case JS::GCReason::CC_FINISHED: {
-      if (XRE_IsContentProcess()) {
-        IdleSchedulerChild* child =
-            IdleSchedulerChild::GetMainThreadIdleScheduler();
-        if (child) {
-          return child->MayGCNow();
-        }
-      }
-      // The parent process doesn't ask IdleSchedulerParent if it can GC.
-      break;
-    }
-    default:
-      break;
-  }
-
-  return MayGCPromise::CreateAndResolve(true, __func__);
-}
-
-void CCGCScheduler::RunNextCollectorTimer(JS::GCReason aReason,
-                                          mozilla::TimeStamp aDeadline) {
-  if (mDidShutdown) {
-    return;
-  }
-
-  // When we're in an incremental GC, we should always have an sGCRunner, so do
-  // not check CC timers. The CC timers won't do anything during a GC.
-  MOZ_ASSERT_IF(InIncrementalGC(), mGCRunner);
-
-  RefPtr<IdleTaskRunner> runner;
-  if (mGCRunner) {
-    SetWantMajorGC(aReason);
-    runner = mGCRunner;
-  } else if (mCCRunner) {
-    runner = mCCRunner;
-  }
-
-  if (runner) {
-    runner->SetIdleDeadline(aDeadline);
-    runner->Run();
-  }
-}
-
-void CCGCScheduler::PokeShrinkingGC() {
-  if (mShrinkingGCTimer || mDidShutdown) {
-    return;
-  }
-
-  NS_NewTimerWithFuncCallback(
-      &mShrinkingGCTimer,
-      [](nsITimer* aTimer, void* aClosure) {
-        static_cast<CCGCScheduler*>(aClosure)->ShrinkingGCTimerFired(aTimer);
-      },
-      this, StaticPrefs::javascript_options_compact_on_user_inactive_delay(),
-      nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY, "ShrinkingGCTimerFired");
-}
-
-void CCGCScheduler::PokeFullGC() {
-  if (!mFullGCTimer && !mDidShutdown) {
-    NS_NewTimerWithFuncCallback(
-        &mFullGCTimer,
-        [](nsITimer* aTimer, void* aClosure) {
-          static_cast<CCGCScheduler*>(aClosure)->FullGCTimerFired(aTimer);
-        },
-        this, StaticPrefs::javascript_options_gc_delay_full(),
-        nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY, "FullGCTimerFired");
-  }
-}
-
-void CCGCScheduler::PokeGC(JS::GCReason aReason, JSObject* aObj,
-                           uint32_t aDelay) {
-  if (mDidShutdown) {
-    return;
-  }
-
-  if (aObj) {
-    JS::Zone* zone = JS::GetObjectZone(aObj);
-    CycleCollectedJSRuntime::Get()->AddZoneWaitingForGC(zone);
-  } else if (aReason != JS::GCReason::CC_FINISHED) {
-    SetNeedsFullGC();
-  }
-
-  if (mGCRunner) {
-    // There's already a runner for GC'ing, just return
-    return;
-  }
-
-  SetWantMajorGC(aReason);
-
-  if (mCCRunner) {
-    // Make sure CC is called regardless of the size of the purple buffer, and
-    // GC after it.
-    EnsureCCThenGC();
-    return;
-  }
-
-  // Wait for javascript.options.gc_delay (or delay_first) then start
-  // looking for idle time to run the initial GC slice.
-  static bool first = true;
-  uint32_t delay =
-      aDelay ? aDelay
-             : (first ? StaticPrefs::javascript_options_gc_delay_first()
-                      : StaticPrefs::javascript_options_gc_delay());
-  first = false;
-  EnsureGCRunner(delay);
-}
-
-void CCGCScheduler::EnsureGCRunner(uint32_t aDelay) {
-  if (mGCRunner) {
-    return;
-  }
-
-  // Wait at most the interslice GC delay before forcing a run.
-  mGCRunner = IdleTaskRunner::Create(
-      [this](TimeStamp aDeadline) { return GCRunnerFired(aDeadline); },
-      "CCGCScheduler::EnsureGCRunner", aDelay,
-      StaticPrefs::javascript_options_gc_delay_interslice(),
-      int64_t(mActiveIntersliceGCBudget.ToMilliseconds()), true,
-      [this] { return mDidShutdown; });
-}
-
-void CCGCScheduler::UserIsInactive() {
-  mUserIsActive = false;
-  if (StaticPrefs::javascript_options_compact_on_user_inactive()) {
-    PokeShrinkingGC();
-  }
-}
-
-void CCGCScheduler::UserIsActive() {
-  mUserIsActive = true;
-  KillShrinkingGCTimer();
-  if (mIsCompactingOnUserInactive) {
-    mozilla::dom::AutoJSAPI jsapi;
-    jsapi.Init();
-    JS::AbortIncrementalGC(jsapi.cx());
-  }
-  MOZ_ASSERT(!mIsCompactingOnUserInactive);
-}
-
-void CCGCScheduler::KillShrinkingGCTimer() {
-  if (mShrinkingGCTimer) {
-    mShrinkingGCTimer->Cancel();
-    NS_RELEASE(mShrinkingGCTimer);
-  }
-}
-
-void CCGCScheduler::KillFullGCTimer() {
-  if (mFullGCTimer) {
-    mFullGCTimer->Cancel();
-    NS_RELEASE(mFullGCTimer);
-  }
-}
-
-void CCGCScheduler::KillGCRunner() {
-  if (mGCRunner) {
-    mGCRunner->Cancel();
-    mGCRunner = nullptr;
-  }
-}
-
-void CCGCScheduler::EnsureCCRunner(TimeDuration aDelay, TimeDuration aBudget) {
-  MOZ_ASSERT(!mDidShutdown);
-
-  if (!mCCRunner) {
-    mCCRunner = IdleTaskRunner::Create(
-        CCRunnerFired, "EnsureCCRunner::CCRunnerFired", 0,
-        aDelay.ToMilliseconds(), aBudget.ToMilliseconds(), true,
-        [this] { return mDidShutdown; });
-  } else {
-    mCCRunner->SetMinimumUsefulBudget(aBudget.ToMilliseconds());
-    nsIEventTarget* target = mozilla::GetCurrentEventTarget();
-    if (target) {
-      mCCRunner->SetTimer(aDelay.ToMilliseconds(), target);
-    }
-  }
-}
-
-void CCGCScheduler::MaybePokeCC() {
-  if (mCCRunner || mDidShutdown) {
-    return;
-  }
-
-  if (ShouldScheduleCC()) {
-    // We can kill some objects before running forgetSkippable.
-    nsCycleCollector_dispatchDeferredDeletion();
-
-    if (!mCCRunner) {
-      InitCCRunnerStateMachine(CCRunnerState::ReducePurple);
-    }
-    EnsureCCRunner(kCCSkippableDelay, kForgetSkippableSliceDuration);
-  }
-}
-
-void CCGCScheduler::KillCCRunner() {
-  UnblockCC();
-  DeactivateCCRunner();
-  if (mCCRunner) {
-    mCCRunner->Cancel();
-    mCCRunner = nullptr;
-  }
-}
-
-void CCGCScheduler::KillAllTimersAndRunners() {
-  KillShrinkingGCTimer();
-  KillCCRunner();
-  KillFullGCTimer();
-  KillGCRunner();
-}
-
-js::SliceBudget CCGCScheduler::ComputeCCSliceBudget(
-    TimeStamp aDeadline, TimeStamp aCCBeginTime, TimeStamp aPrevSliceEndTime,
-    bool* aPreferShorterSlices) const {
-  TimeStamp now = Now();
-
-  *aPreferShorterSlices =
-      aDeadline.IsNull() || (aDeadline - now) < kICCSliceBudget;
-
-  TimeDuration baseBudget =
-      aDeadline.IsNull() ? kICCSliceBudget : aDeadline - now;
-
-  if (aCCBeginTime.IsNull()) {
-    // If no CC is in progress, use the standard slice time.
-    return js::SliceBudget(js::TimeBudget(baseBudget),
-                           kNumCCNodesBetweenTimeChecks);
-  }
-
-  // Only run a limited slice if we're within the max running time.
-  MOZ_ASSERT(now >= aCCBeginTime);
-  TimeDuration runningTime = now - aCCBeginTime;
-  if (runningTime >= kMaxICCDuration) {
-    return js::SliceBudget::unlimited();
-  }
-
-  const TimeDuration maxSlice =
-      TimeDuration::FromMilliseconds(MainThreadIdlePeriod::GetLongIdlePeriod());
-
-  // Try to make up for a delay in running this slice.
-  MOZ_ASSERT(now >= aPrevSliceEndTime);
-  double sliceDelayMultiplier = (now - aPrevSliceEndTime) / kICCIntersliceDelay;
-  TimeDuration delaySliceBudget =
-      std::min(baseBudget.MultDouble(sliceDelayMultiplier), maxSlice);
-
-  // Increase slice budgets up to |maxSlice| as we approach
-  // half way through the ICC, to avoid large sync CCs.
-  double percentToHalfDone =
-      std::min(2.0 * (runningTime / kMaxICCDuration), 1.0);
-  TimeDuration laterSliceBudget = maxSlice.MultDouble(percentToHalfDone);
-
-  // Note: We may have already overshot the deadline, in which case
-  // baseBudget will be negative and we will end up returning
-  // laterSliceBudget.
-  return js::SliceBudget(js::TimeBudget(std::max(
-                             {delaySliceBudget, laterSliceBudget, baseBudget})),
-                         kNumCCNodesBetweenTimeChecks);
-}
-
-TimeDuration CCGCScheduler::ComputeInterSliceGCBudget(TimeStamp aDeadline,
-                                                      TimeStamp aNow) const {
-  // We use longer budgets when the CC has been locked out but the CC has
-  // tried to run since that means we may have a significant amount of
-  // garbage to collect and it's better to GC in several longer slices than
-  // in a very long one.
-  TimeDuration budget =
-      aDeadline.IsNull() ? mActiveIntersliceGCBudget * 2 : aDeadline - aNow;
-  if (!mCCBlockStart) {
-    return budget;
-  }
-
-  TimeDuration blockedTime = aNow - mCCBlockStart;
-  TimeDuration maxSliceGCBudget = mActiveIntersliceGCBudget * 10;
-  double percentOfBlockedTime =
-      std::min(blockedTime / kMaxCCLockedoutTime, 1.0);
-  return std::max(budget, maxSliceGCBudget.MultDouble(percentOfBlockedTime));
-}
-
-bool CCGCScheduler::ShouldScheduleCC() const {
-  if (!mHasRunGC) {
-    return false;
-  }
-
-  TimeStamp now = Now();
-
-  // Don't run consecutive CCs too often.
-  if (mCleanupsSinceLastGC && !mLastCCEndTime.IsNull()) {
-    if (now - mLastCCEndTime < kCCDelay) {
-      return false;
-    }
-  }
-
-  // If GC hasn't run recently and forget skippable only cycle was run,
-  // don't start a new cycle too soon.
-  if ((mCleanupsSinceLastGC > kMajorForgetSkippableCalls) &&
-      !mLastForgetSkippableCycleEndTime.IsNull()) {
-    if (now - mLastForgetSkippableCycleEndTime <
-        kTimeBetweenForgetSkippableCycles) {
-      return false;
-    }
-  }
-
-  return IsCCNeeded(now);
-}
-
-CCRunnerStep CCGCScheduler::AdvanceCCRunner(TimeStamp aDeadline) {
-  struct StateDescriptor {
-    // When in this state, should we first check to see if we still have
-    // enough reason to CC?
-    bool mCanAbortCC;
-
-    // If we do decide to abort the CC, should we still try to forget
-    // skippables one more time?
-    bool mTryFinalForgetSkippable;
-  };
-
-  // The state descriptors for Inactive and Canceled will never actually be
-  // used. We will never call this function while Inactive, and Canceled is
-  // handled specially at the beginning.
-  constexpr StateDescriptor stateDescriptors[] = {
-      {false, false},  /* CCRunnerState::Inactive */
-      {false, false},  /* CCRunnerState::ReducePurple */
-      {true, true},    /* CCRunnerState::CleanupChildless */
-      {true, false},   /* CCRunnerState::CleanupContentUnbinder */
-      {false, false},  /* CCRunnerState::CleanupDeferred */
-      {false, false},  /* CCRunnerState::StartCycleCollection */
-      {false, false},  /* CCRunnerState::CycleCollecting */
-      {false, false}}; /* CCRunnerState::Canceled */
-  static_assert(
-      ArrayLength(stateDescriptors) == size_t(CCRunnerState::NumStates),
-      "need one state descriptor per state");
-  const StateDescriptor& desc = stateDescriptors[int(mCCRunnerState)];
-
-  // Make sure we initialized the state machine.
-  MOZ_ASSERT(mCCRunnerState != CCRunnerState::Inactive);
-
-  if (mDidShutdown) {
-    return {CCRunnerAction::StopRunning, Yield};
-  }
-
-  if (mCCRunnerState == CCRunnerState::Canceled) {
-    // When we cancel a cycle, there may have been a final ForgetSkippable.
-    return {CCRunnerAction::StopRunning, Yield};
-  }
-
-  TimeStamp now = Now();
-
-  if (InIncrementalGC()) {
-    if (mCCBlockStart.IsNull()) {
-      BlockCC(now);
-
-      // If we have reached the CycleCollecting state, then ignore CC timer
-      // fires while incremental GC is running. (Running ICC during an IGC
-      // would cause us to synchronously finish the GC, which is bad.)
-      //
-      // If we have not yet started cycle collecting, then reset our state so
-      // that we run forgetSkippable often enough before CC. Because of reduced
-      // mCCDelay, forgetSkippable will be called just a few times.
-      //
-      // The kMaxCCLockedoutTime limit guarantees that we end up calling
-      // forgetSkippable and CycleCollectNow eventually.
-
-      if (mCCRunnerState != CCRunnerState::CycleCollecting) {
-        mCCRunnerState = CCRunnerState::ReducePurple;
-        mCCRunnerEarlyFireCount = 0;
-        mCCDelay = kCCDelay / int64_t(3);
-      }
-      return {CCRunnerAction::None, Yield};
-    }
-
-    if (GetCCBlockedTime(now) < kMaxCCLockedoutTime) {
-      return {CCRunnerAction::None, Yield};
-    }
-
-    // Locked out for too long, so proceed and finish the incremental GC
-    // synchronously.
-  }
-
-  // For states that aren't just continuations of previous states, check
-  // whether a CC is still needed (after doing various things to reduce the
-  // purple buffer).
-  if (desc.mCanAbortCC && !IsCCNeeded(now)) {
-    // If we don't pass the threshold for wanting to cycle collect, stop now
-    // (after possibly doing a final ForgetSkippable).
-    mCCRunnerState = CCRunnerState::Canceled;
-    NoteForgetSkippableOnlyCycle();
-
-    // Preserve the previous code's idea of when to check whether a
-    // ForgetSkippable should be fired.
-    if (desc.mTryFinalForgetSkippable && ShouldForgetSkippable()) {
-      // The Canceled state will make us StopRunning after this action is
-      // performed (see conditional at top of function).
-      return {CCRunnerAction::ForgetSkippable, Yield, KeepChildless};
-    }
-
-    return {CCRunnerAction::StopRunning, Yield};
-  }
-
-  switch (mCCRunnerState) {
-      // ReducePurple: a GC ran (or we otherwise decided to try CC'ing). Wait
-      // for some amount of time (kCCDelay, or less if incremental GC blocked
-      // this CC) while firing regular ForgetSkippable actions before continuing
-      // on.
-    case CCRunnerState::ReducePurple:
-      ++mCCRunnerEarlyFireCount;
-      if (IsLastEarlyCCTimer(mCCRunnerEarlyFireCount)) {
-        mCCRunnerState = CCRunnerState::CleanupChildless;
-      }
-
-      if (ShouldForgetSkippable()) {
-        return {CCRunnerAction::ForgetSkippable, Yield, KeepChildless};
-      }
-
-      if (aDeadline.IsNull()) {
-        return {CCRunnerAction::None, Yield};
-      }
-
-      // If we're called during idle time, try to find some work to do by
-      // advancing to the next state, effectively bypassing some possible forget
-      // skippable calls.
-      mCCRunnerState = CCRunnerState::CleanupChildless;
-
-      // Continue on to CleanupChildless, but only after checking IsCCNeeded
-      // again.
-      return {CCRunnerAction::None, Continue};
-
-      // CleanupChildless: do a stronger ForgetSkippable that removes nodes with
-      // no children in the cycle collector graph. This state is split into 3
-      // parts; the other Cleanup* actions will happen within the same callback
-      // (unless the ForgetSkippable shrinks the purple buffer enough for the CC
-      // to be skipped entirely.)
-    case CCRunnerState::CleanupChildless:
-      mCCRunnerState = CCRunnerState::CleanupContentUnbinder;
-      return {CCRunnerAction::ForgetSkippable, Yield, RemoveChildless};
-
-      // CleanupContentUnbinder: continuing cleanup, clear out the content
-      // unbinder.
-    case CCRunnerState::CleanupContentUnbinder:
-      if (aDeadline.IsNull()) {
-        // Non-idle (waiting) callbacks skip the rest of the cleanup, but still
-        // wait for another fire before the actual CC.
-        mCCRunnerState = CCRunnerState::StartCycleCollection;
-        return {CCRunnerAction::None, Yield};
-      }
-
-      // Running in an idle callback.
-
-      // The deadline passed, so go straight to CC in the next slice.
-      if (now >= aDeadline) {
-        mCCRunnerState = CCRunnerState::StartCycleCollection;
-        return {CCRunnerAction::None, Yield};
-      }
-
-      mCCRunnerState = CCRunnerState::CleanupDeferred;
-      return {CCRunnerAction::CleanupContentUnbinder, Continue};
-
-      // CleanupDeferred: continuing cleanup, do deferred deletion.
-    case CCRunnerState::CleanupDeferred:
-      MOZ_ASSERT(!aDeadline.IsNull(),
-                 "Should only be in CleanupDeferred state when idle");
-
-      // Our efforts to avoid a CC have failed. Let the timer fire once more
-      // to trigger a CC.
-      mCCRunnerState = CCRunnerState::StartCycleCollection;
-      if (now >= aDeadline) {
-        // The deadline passed, go straight to CC in the next slice.
-        return {CCRunnerAction::None, Yield};
-      }
-
-      return {CCRunnerAction::CleanupDeferred, Yield};
-
-      // StartCycleCollection: start actually doing cycle collection slices.
-    case CCRunnerState::StartCycleCollection:
-      // We are in the final timer fire and still meet the conditions for
-      // triggering a CC. Let RunCycleCollectorSlice finish the current IGC if
-      // any, because that will allow us to include the GC time in the CC pause.
-      mCCRunnerState = CCRunnerState::CycleCollecting;
-      [[fallthrough]];
-
-      // CycleCollecting: continue running slices until done.
-    case CCRunnerState::CycleCollecting:
-      return {CCRunnerAction::CycleCollect, Yield};
-
-    default:
-      MOZ_CRASH("Unexpected CCRunner state");
-  };
-}
-
-GCRunnerStep CCGCScheduler::GetNextGCRunnerAction(TimeStamp aDeadline) {
-  MOZ_ASSERT(mMajorGCReason != JS::GCReason::NO_REASON);
-
-  if (InIncrementalGC()) {
-    return {GCRunnerAction::GCSlice, mMajorGCReason};
-  }
-
-  if (mReadyForMajorGC) {
-    return {GCRunnerAction::StartMajorGC, mMajorGCReason};
-  }
-
-  return {GCRunnerAction::WaitToMajorGC, mMajorGCReason};
-}
-
-js::SliceBudget CCGCScheduler::ComputeForgetSkippableBudget(
-    TimeStamp aStartTimeStamp, TimeStamp aDeadline) {
-  if (mForgetSkippableFrequencyStartTime.IsNull()) {
-    mForgetSkippableFrequencyStartTime = aStartTimeStamp;
-  } else if (aStartTimeStamp - mForgetSkippableFrequencyStartTime >
-             kOneMinute) {
-    TimeStamp startPlusMinute = mForgetSkippableFrequencyStartTime + kOneMinute;
-
-    // If we had forget skippables only at the beginning of the interval, we
-    // still want to use the whole time, minute or more, for frequency
-    // calculation. mLastForgetSkippableEndTime is needed if forget skippable
-    // takes enough time to push the interval to be over a minute.
-    TimeStamp endPoint = std::max(startPlusMinute, mLastForgetSkippableEndTime);
-
-    // Duration in minutes.
-    double duration =
-        (endPoint - mForgetSkippableFrequencyStartTime).ToSeconds() / 60;
-    uint32_t frequencyPerMinute = uint32_t(mForgetSkippableCounter / duration);
-    Telemetry::Accumulate(Telemetry::FORGET_SKIPPABLE_FREQUENCY,
-                          frequencyPerMinute);
-    mForgetSkippableCounter = 0;
-    mForgetSkippableFrequencyStartTime = aStartTimeStamp;
-  }
-  ++mForgetSkippableCounter;
-
-  TimeDuration budgetTime =
-      aDeadline ? (aDeadline - aStartTimeStamp) : kForgetSkippableSliceDuration;
-  return js::SliceBudget(budgetTime);
-}
-
-}  // namespace mozilla
--- a/dom/base/CCGCScheduler.h
+++ b/dom/base/CCGCScheduler.h
@@ -1,20 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "js/SliceBudget.h"
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/CycleCollectedJSContext.h"
-#include "mozilla/IdleTaskRunner.h"
 #include "mozilla/MainThreadIdlePeriod.h"
 #include "mozilla/Telemetry.h"
 #include "mozilla/TimeStamp.h"
-#include "mozilla/ipc/IdleSchedulerChild.h"
 #include "nsCycleCollector.h"
 #include "nsJSEnvironment.h"
 
 namespace mozilla {
 
 static const TimeDuration kOneMinute = TimeDuration::FromSeconds(60.0f);
 
 // The amount of time we wait between a request to CC (after GC ran)
@@ -107,24 +105,22 @@ struct CCRunnerStep {
 };
 
 class CCGCScheduler {
  public:
   // Mockable functions to interface with the code being scheduled.
 
   // Current time. In real usage, this will just return TimeStamp::Now(), but
   // tests can reimplement it to return a value controlled by the test.
-  static TimeStamp Now();
+  static inline TimeStamp Now();
 
   // Number of entries in the purple buffer (those objects whose ref counts
   // have been decremented since the previous CC, roughly), and are therefore
   // "suspected" of being members of cyclic garbage.
-  static uint32_t SuspectedCCObjects();
-
-  static bool CCRunnerFired(TimeStamp aDeadline);
+  static inline uint32_t SuspectedCCObjects();
 
   // Parameter setting
 
   void SetActiveIntersliceGCBudget(TimeDuration aDuration) {
     mActiveIntersliceGCBudget = aDuration;
   }
 
   // State retrieval
@@ -140,40 +136,16 @@ class CCGCScheduler {
   TimeStamp GetLastCCEndTime() const { return mLastCCEndTime; }
 
   bool IsEarlyForgetSkippable(uint32_t aN = kMajorForgetSkippableCalls) const {
     return mCleanupsSinceLastGC < aN;
   }
 
   bool NeedsFullGC() const { return mNeedsFullGC; }
 
-  // Requests
-  void PokeGC(JS::GCReason aReason, JSObject* aObj, uint32_t aDelay = 0);
-  void PokeShrinkingGC();
-  void PokeFullGC();
-  void MaybePokeCC();
-
-  void UserIsInactive();
-  void UserIsActive();
-
-  void KillShrinkingGCTimer();
-  void KillFullGCTimer();
-  void KillGCRunner();
-  void KillCCRunner();
-  void KillAllTimersAndRunners();
-
-  /*
-   * aDelay is the delay before the first time the idle task runner runs.
-   * Then it runs every
-   * StaticPrefs::javascript_options_gc_delay_interslice()
-   */
-  void EnsureGCRunner(uint32_t aDelay);
-
-  void EnsureCCRunner(TimeDuration aDelay, TimeDuration aBudget);
-
   // State modification
 
   void SetNeedsFullGC(bool aNeedGC = true) { mNeedsFullGC = aNeedGC; }
 
   void SetWantMajorGC(JS::GCReason aReason) {
     mMajorGCReason = aReason;
 
     // Force full GCs when called from reftests so that we collect dead zones
@@ -211,55 +183,36 @@ class CCGCScheduler {
     mMajorGCReason = JS::GCReason::NO_REASON;
 
     mInIncrementalGC = false;
     mCCBlockStart = TimeStamp();
     mInIncrementalGC = false;
     mReadyForMajorGC = false;
     mNeedsFullCC = true;
     mHasRunGC = true;
-    mIsCompactingOnUserInactive = false;
 
     mCleanupsSinceLastGC = 0;
     mCCollectedWaitingForGC = 0;
     mCCollectedZonesWaitingForGC = 0;
     mLikelyShortLivingObjectsNeedingGC = 0;
   }
 
-  void NoteGCSliceEnd(TimeDuration aSliceDuration) {
+  void NoteGCSliceEnd() {
     if (mMajorGCReason == JS::GCReason::NO_REASON) {
       // Internally-triggered GCs do not wait for the parent's permission to
       // proceed. This flag won't be checked during an incremental GC anyway,
       // but it better reflects reality.
       mReadyForMajorGC = true;
     }
 
     // Subsequent slices should be INTER_SLICE_GC unless they are triggered by
     // something else that provides its own reason.
     mMajorGCReason = JS::GCReason::INTER_SLICE_GC;
-
-    mGCUnnotifiedTotalTime += aSliceDuration;
   }
 
-  void FullGCTimerFired(nsITimer* aTimer);
-  void ShrinkingGCTimerFired(nsITimer* aTimer);
-  bool GCRunnerFired(TimeStamp aDeadline);
-
-  using MayGCPromise =
-      MozPromise<bool /* aIgnored */, mozilla::ipc::ResponseRejectReason, true>;
-
-  // Returns null if we shouldn't GC now (eg a GC is already running).
-  static RefPtr<MayGCPromise> MayGCNow(JS::GCReason reason);
-
-  // Check all of the various collector timers/runners and see if they are
-  // waiting to fire. This does not check the Full GC Timer, as that's a
-  // more expensive collection we run on a long timer.
-  void RunNextCollectorTimer(JS::GCReason aReason,
-                             mozilla::TimeStamp aDeadline);
-
   // When we decide to do a cycle collection but we're in the middle of an
   // incremental GC, the CC is "locked out" until the GC completes -- unless
   // the wait is too long, and we decide to finish the incremental GC early.
   void BlockCC(TimeStamp aNow) {
     MOZ_ASSERT(mInIncrementalGC);
     MOZ_ASSERT(mCCBlockStart.IsNull());
     mCCBlockStart = aNow;
   }
@@ -296,33 +249,30 @@ class CCGCScheduler {
   }
 
   // The CC was abandoned without running a slice, so we only did forget
   // skippables. Prevent running another cycle soon.
   void NoteForgetSkippableOnlyCycle() {
     mLastForgetSkippableCycleEndTime = Now();
   }
 
-  void Shutdown() {
-    mDidShutdown = true;
-    KillAllTimersAndRunners();
-  }
+  void Shutdown() { mDidShutdown = true; }
 
   // Scheduling
 
   // Return a budget along with a boolean saying whether to prefer to run short
   // slices and stop rather than continuing to the next phase of cycle
   // collection.
-  js::SliceBudget ComputeCCSliceBudget(TimeStamp aDeadline,
-                                       TimeStamp aCCBeginTime,
-                                       TimeStamp aPrevSliceEndTime,
-                                       bool* aPreferShorterSlices) const;
+  inline js::SliceBudget ComputeCCSliceBudget(TimeStamp aDeadline,
+                                              TimeStamp aCCBeginTime,
+                                              TimeStamp aPrevSliceEndTime,
+                                              bool* aPreferShorterSlices) const;
 
-  TimeDuration ComputeInterSliceGCBudget(TimeStamp aDeadline,
-                                         TimeStamp aNow) const;
+  inline TimeDuration ComputeInterSliceGCBudget(TimeStamp aDeadline,
+                                                TimeStamp aNow) const;
 
   bool ShouldForgetSkippable() const {
     // Only do a forget skippable if there are more than a few new objects
     // or we're doing the initial forget skippables.
     return ((mPreviousSuspectedCount + 100) <= SuspectedCCObjects()) ||
            mCleanupsSinceLastGC < kMajorForgetSkippableCalls;
   }
 
@@ -335,17 +285,17 @@ class CCGCScheduler {
       return true;
     }
     uint32_t suspected = SuspectedCCObjects();
     return suspected > kCCPurpleLimit ||
            (suspected > kCCForcedPurpleLimit && mLastCCEndTime &&
             aNow - mLastCCEndTime > kCCForced);
   }
 
-  bool ShouldScheduleCC() const;
+  inline bool ShouldScheduleCC() const;
 
   // If we collected a substantial amount of cycles, poke the GC since more
   // objects might be unreachable now.
   bool NeedsGCAfterCC() const {
     return mCCollectedWaitingForGC > 250 || mCCollectedZonesWaitingForGC > 0 ||
            mLikelyShortLivingObjectsNeedingGC > 2500 || mNeedsGCAfterCC;
   }
 
@@ -364,20 +314,16 @@ class CCGCScheduler {
     CleanupDeferred,
     StartCycleCollection,
     CycleCollecting,
     Canceled,
     NumStates
   };
 
   void InitCCRunnerStateMachine(CCRunnerState initialState) {
-    if (mCCRunner) {
-      return;
-    }
-
     // The state machine should always have been deactivated after the previous
     // collection, however far that collection may have gone.
     MOZ_ASSERT(mCCRunnerState == CCRunnerState::Inactive,
                "DeactivateCCRunner should have been called");
     mCCRunnerState = initialState;
 
     // Currently, there are only two entry points to the non-Inactive part of
     // the state machine.
@@ -388,19 +334,19 @@ class CCGCScheduler {
       // Nothing needed.
     } else {
       MOZ_CRASH("Invalid initial state");
     }
   }
 
   void DeactivateCCRunner() { mCCRunnerState = CCRunnerState::Inactive; }
 
-  GCRunnerStep GetNextGCRunnerAction(TimeStamp aDeadline);
+  inline GCRunnerStep GetNextGCRunnerAction(TimeStamp aDeadline);
 
-  CCRunnerStep AdvanceCCRunner(TimeStamp aDeadline);
+  inline CCRunnerStep AdvanceCCRunner(TimeStamp aDeadline);
 
   // aStartTimeStamp : when the ForgetSkippable timer fired. This may be some
   // time ago, if an incremental GC needed to be finished.
   js::SliceBudget ComputeForgetSkippableBudget(TimeStamp aStartTimeStamp,
                                                TimeStamp aDeadline);
 
  private:
   // State
@@ -434,31 +380,341 @@ class CCGCScheduler {
 
   bool mNeedsFullCC = false;
   bool mNeedsFullGC = true;
   bool mNeedsGCAfterCC = false;
   uint32_t mPreviousSuspectedCount = 0;
 
   uint32_t mCleanupsSinceLastGC = UINT32_MAX;
 
-  TimeDuration mGCUnnotifiedTotalTime;
-
-  RefPtr<IdleTaskRunner> mGCRunner;
-  RefPtr<IdleTaskRunner> mCCRunner;
-  nsITimer* mShrinkingGCTimer = nullptr;
-  nsITimer* mFullGCTimer = nullptr;
-
   JS::GCReason mMajorGCReason = JS::GCReason::NO_REASON;
 
-  bool mIsCompactingOnUserInactive = false;
-  bool mUserIsActive = true;
-
  public:
   uint32_t mCCollectedWaitingForGC = 0;
   uint32_t mCCollectedZonesWaitingForGC = 0;
   uint32_t mLikelyShortLivingObjectsNeedingGC = 0;
 
   // Configuration parameters
 
   TimeDuration mActiveIntersliceGCBudget = TimeDuration::FromMilliseconds(5);
 };
 
+js::SliceBudget CCGCScheduler::ComputeCCSliceBudget(
+    TimeStamp aDeadline, TimeStamp aCCBeginTime, TimeStamp aPrevSliceEndTime,
+    bool* aPreferShorterSlices) const {
+  TimeStamp now = Now();
+
+  *aPreferShorterSlices =
+      aDeadline.IsNull() || (aDeadline - now) < kICCSliceBudget;
+
+  TimeDuration baseBudget =
+      aDeadline.IsNull() ? kICCSliceBudget : aDeadline - now;
+
+  if (aCCBeginTime.IsNull()) {
+    // If no CC is in progress, use the standard slice time.
+    return js::SliceBudget(js::TimeBudget(baseBudget),
+                           kNumCCNodesBetweenTimeChecks);
+  }
+
+  // Only run a limited slice if we're within the max running time.
+  MOZ_ASSERT(now >= aCCBeginTime);
+  TimeDuration runningTime = now - aCCBeginTime;
+  if (runningTime >= kMaxICCDuration) {
+    return js::SliceBudget::unlimited();
+  }
+
+  const TimeDuration maxSlice =
+      TimeDuration::FromMilliseconds(MainThreadIdlePeriod::GetLongIdlePeriod());
+
+  // Try to make up for a delay in running this slice.
+  MOZ_ASSERT(now >= aPrevSliceEndTime);
+  double sliceDelayMultiplier = (now - aPrevSliceEndTime) / kICCIntersliceDelay;
+  TimeDuration delaySliceBudget =
+      std::min(baseBudget.MultDouble(sliceDelayMultiplier), maxSlice);
+
+  // Increase slice budgets up to |maxSlice| as we approach
+  // half way through the ICC, to avoid large sync CCs.
+  double percentToHalfDone =
+      std::min(2.0 * (runningTime / kMaxICCDuration), 1.0);
+  TimeDuration laterSliceBudget = maxSlice.MultDouble(percentToHalfDone);
+
+  // Note: We may have already overshot the deadline, in which case
+  // baseBudget will be negative and we will end up returning
+  // laterSliceBudget.
+  return js::SliceBudget(js::TimeBudget(std::max(
+                             {delaySliceBudget, laterSliceBudget, baseBudget})),
+                         kNumCCNodesBetweenTimeChecks);
+}
+
+inline TimeDuration CCGCScheduler::ComputeInterSliceGCBudget(
+    TimeStamp aDeadline, TimeStamp aNow) const {
+  // We use longer budgets when the CC has been locked out but the CC has
+  // tried to run since that means we may have a significant amount of
+  // garbage to collect and it's better to GC in several longer slices than
+  // in a very long one.
+  TimeDuration budget =
+      aDeadline.IsNull() ? mActiveIntersliceGCBudget * 2 : aDeadline - aNow;
+  if (!mCCBlockStart) {
+    return budget;
+  }
+
+  TimeDuration blockedTime = aNow - mCCBlockStart;
+  TimeDuration maxSliceGCBudget = mActiveIntersliceGCBudget * 10;
+  double percentOfBlockedTime =
+      std::min(blockedTime / kMaxCCLockedoutTime, 1.0);
+  return std::max(budget, maxSliceGCBudget.MultDouble(percentOfBlockedTime));
+}
+
+bool CCGCScheduler::ShouldScheduleCC() const {
+  if (!mHasRunGC) {
+    return false;
+  }
+
+  TimeStamp now = Now();
+
+  // Don't run consecutive CCs too often.
+  if (mCleanupsSinceLastGC && !mLastCCEndTime.IsNull()) {
+    if (now - mLastCCEndTime < kCCDelay) {
+      return false;
+    }
+  }
+
+  // If GC hasn't run recently and forget skippable only cycle was run,
+  // don't start a new cycle too soon.
+  if ((mCleanupsSinceLastGC > kMajorForgetSkippableCalls) &&
+      !mLastForgetSkippableCycleEndTime.IsNull()) {
+    if (now - mLastForgetSkippableCycleEndTime <
+        kTimeBetweenForgetSkippableCycles) {
+      return false;
+    }
+  }
+
+  return IsCCNeeded(now);
+}
+
+CCRunnerStep CCGCScheduler::AdvanceCCRunner(TimeStamp aDeadline) {
+  struct StateDescriptor {
+    // When in this state, should we first check to see if we still have
+    // enough reason to CC?
+    bool mCanAbortCC;
+
+    // If we do decide to abort the CC, should we still try to forget
+    // skippables one more time?
+    bool mTryFinalForgetSkippable;
+  };
+
+  // The state descriptors for Inactive and Canceled will never actually be
+  // used. We will never call this function while Inactive, and Canceled is
+  // handled specially at the beginning.
+  constexpr StateDescriptor stateDescriptors[] = {
+      {false, false},  /* CCRunnerState::Inactive */
+      {false, false},  /* CCRunnerState::ReducePurple */
+      {true, true},    /* CCRunnerState::CleanupChildless */
+      {true, false},   /* CCRunnerState::CleanupContentUnbinder */
+      {false, false},  /* CCRunnerState::CleanupDeferred */
+      {false, false},  /* CCRunnerState::StartCycleCollection */
+      {false, false},  /* CCRunnerState::CycleCollecting */
+      {false, false}}; /* CCRunnerState::Canceled */
+  static_assert(
+      ArrayLength(stateDescriptors) == size_t(CCRunnerState::NumStates),
+      "need one state descriptor per state");
+  const StateDescriptor& desc = stateDescriptors[int(mCCRunnerState)];
+
+  // Make sure we initialized the state machine.
+  MOZ_ASSERT(mCCRunnerState != CCRunnerState::Inactive);
+
+  if (mDidShutdown) {
+    return {CCRunnerAction::StopRunning, Yield};
+  }
+
+  if (mCCRunnerState == CCRunnerState::Canceled) {
+    // When we cancel a cycle, there may have been a final ForgetSkippable.
+    return {CCRunnerAction::StopRunning, Yield};
+  }
+
+  TimeStamp now = Now();
+
+  if (InIncrementalGC()) {
+    if (mCCBlockStart.IsNull()) {
+      BlockCC(now);
+
+      // If we have reached the CycleCollecting state, then ignore CC timer
+      // fires while incremental GC is running. (Running ICC during an IGC
+      // would cause us to synchronously finish the GC, which is bad.)
+      //
+      // If we have not yet started cycle collecting, then reset our state so
+      // that we run forgetSkippable often enough before CC. Because of reduced
+      // mCCDelay, forgetSkippable will be called just a few times.
+      //
+      // The kMaxCCLockedoutTime limit guarantees that we end up calling
+      // forgetSkippable and CycleCollectNow eventually.
+
+      if (mCCRunnerState != CCRunnerState::CycleCollecting) {
+        mCCRunnerState = CCRunnerState::ReducePurple;
+        mCCRunnerEarlyFireCount = 0;
+        mCCDelay = kCCDelay / int64_t(3);
+      }
+      return {CCRunnerAction::None, Yield};
+    }
+
+    if (GetCCBlockedTime(now) < kMaxCCLockedoutTime) {
+      return {CCRunnerAction::None, Yield};
+    }
+
+    // Locked out for too long, so proceed and finish the incremental GC
+    // synchronously.
+  }
+
+  // For states that aren't just continuations of previous states, check
+  // whether a CC is still needed (after doing various things to reduce the
+  // purple buffer).
+  if (desc.mCanAbortCC && !IsCCNeeded(now)) {
+    // If we don't pass the threshold for wanting to cycle collect, stop now
+    // (after possibly doing a final ForgetSkippable).
+    mCCRunnerState = CCRunnerState::Canceled;
+    NoteForgetSkippableOnlyCycle();
+
+    // Preserve the previous code's idea of when to check whether a
+    // ForgetSkippable should be fired.
+    if (desc.mTryFinalForgetSkippable && ShouldForgetSkippable()) {
+      // The Canceled state will make us StopRunning after this action is
+      // performed (see conditional at top of function).
+      return {CCRunnerAction::ForgetSkippable, Yield, KeepChildless};
+    }
+
+    return {CCRunnerAction::StopRunning, Yield};
+  }
+
+  switch (mCCRunnerState) {
+      // ReducePurple: a GC ran (or we otherwise decided to try CC'ing). Wait
+      // for some amount of time (kCCDelay, or less if incremental GC blocked
+      // this CC) while firing regular ForgetSkippable actions before continuing
+      // on.
+    case CCRunnerState::ReducePurple:
+      ++mCCRunnerEarlyFireCount;
+      if (IsLastEarlyCCTimer(mCCRunnerEarlyFireCount)) {
+        mCCRunnerState = CCRunnerState::CleanupChildless;
+      }
+
+      if (ShouldForgetSkippable()) {
+        return {CCRunnerAction::ForgetSkippable, Yield, KeepChildless};
+      }
+
+      if (aDeadline.IsNull()) {
+        return {CCRunnerAction::None, Yield};
+      }
+
+      // If we're called during idle time, try to find some work to do by
+      // advancing to the next state, effectively bypassing some possible forget
+      // skippable calls.
+      mCCRunnerState = CCRunnerState::CleanupChildless;
+
+      // Continue on to CleanupChildless, but only after checking IsCCNeeded
+      // again.
+      return {CCRunnerAction::None, Continue};
+
+      // CleanupChildless: do a stronger ForgetSkippable that removes nodes with
+      // no children in the cycle collector graph. This state is split into 3
+      // parts; the other Cleanup* actions will happen within the same callback
+      // (unless the ForgetSkippable shrinks the purple buffer enough for the CC
+      // to be skipped entirely.)
+    case CCRunnerState::CleanupChildless:
+      mCCRunnerState = CCRunnerState::CleanupContentUnbinder;
+      return {CCRunnerAction::ForgetSkippable, Yield, RemoveChildless};
+
+      // CleanupContentUnbinder: continuing cleanup, clear out the content
+      // unbinder.
+    case CCRunnerState::CleanupContentUnbinder:
+      if (aDeadline.IsNull()) {
+        // Non-idle (waiting) callbacks skip the rest of the cleanup, but still
+        // wait for another fire before the actual CC.
+        mCCRunnerState = CCRunnerState::StartCycleCollection;
+        return {CCRunnerAction::None, Yield};
+      }
+
+      // Running in an idle callback.
+
+      // The deadline passed, so go straight to CC in the next slice.
+      if (now >= aDeadline) {
+        mCCRunnerState = CCRunnerState::StartCycleCollection;
+        return {CCRunnerAction::None, Yield};
+      }
+
+      mCCRunnerState = CCRunnerState::CleanupDeferred;
+      return {CCRunnerAction::CleanupContentUnbinder, Continue};
+
+      // CleanupDeferred: continuing cleanup, do deferred deletion.
+    case CCRunnerState::CleanupDeferred:
+      MOZ_ASSERT(!aDeadline.IsNull(),
+                 "Should only be in CleanupDeferred state when idle");
+
+      // Our efforts to avoid a CC have failed. Let the timer fire once more
+      // to trigger a CC.
+      mCCRunnerState = CCRunnerState::StartCycleCollection;
+      if (now >= aDeadline) {
+        // The deadline passed, go straight to CC in the next slice.
+        return {CCRunnerAction::None, Yield};
+      }
+
+      return {CCRunnerAction::CleanupDeferred, Yield};
+
+      // StartCycleCollection: start actually doing cycle collection slices.
+    case CCRunnerState::StartCycleCollection:
+      // We are in the final timer fire and still meet the conditions for
+      // triggering a CC. Let RunCycleCollectorSlice finish the current IGC if
+      // any, because that will allow us to include the GC time in the CC pause.
+      mCCRunnerState = CCRunnerState::CycleCollecting;
+      [[fallthrough]];
+
+      // CycleCollecting: continue running slices until done.
+    case CCRunnerState::CycleCollecting:
+      return {CCRunnerAction::CycleCollect, Yield};
+
+    default:
+      MOZ_CRASH("Unexpected CCRunner state");
+  };
+}
+
+GCRunnerStep CCGCScheduler::GetNextGCRunnerAction(TimeStamp aDeadline) {
+  MOZ_ASSERT(mMajorGCReason != JS::GCReason::NO_REASON);
+
+  if (InIncrementalGC()) {
+    return {GCRunnerAction::GCSlice, mMajorGCReason};
+  }
+
+  if (mReadyForMajorGC) {
+    return {GCRunnerAction::StartMajorGC, mMajorGCReason};
+  }
+
+  return {GCRunnerAction::WaitToMajorGC, mMajorGCReason};
+}
+
+inline js::SliceBudget CCGCScheduler::ComputeForgetSkippableBudget(
+    TimeStamp aStartTimeStamp, TimeStamp aDeadline) {
+  if (mForgetSkippableFrequencyStartTime.IsNull()) {
+    mForgetSkippableFrequencyStartTime = aStartTimeStamp;
+  } else if (aStartTimeStamp - mForgetSkippableFrequencyStartTime >
+             kOneMinute) {
+    TimeStamp startPlusMinute = mForgetSkippableFrequencyStartTime + kOneMinute;
+
+    // If we had forget skippables only at the beginning of the interval, we
+    // still want to use the whole time, minute or more, for frequency
+    // calculation. mLastForgetSkippableEndTime is needed if forget skippable
+    // takes enough time to push the interval to be over a minute.
+    TimeStamp endPoint = std::max(startPlusMinute, mLastForgetSkippableEndTime);
+
+    // Duration in minutes.
+    double duration =
+        (endPoint - mForgetSkippableFrequencyStartTime).ToSeconds() / 60;
+    uint32_t frequencyPerMinute = uint32_t(mForgetSkippableCounter / duration);
+    Telemetry::Accumulate(Telemetry::FORGET_SKIPPABLE_FREQUENCY,
+                          frequencyPerMinute);
+    mForgetSkippableCounter = 0;
+    mForgetSkippableFrequencyStartTime = aStartTimeStamp;
+  }
+  ++mForgetSkippableCounter;
+
+  TimeDuration budgetTime =
+      aDeadline ? (aDeadline - aStartTimeStamp) : kForgetSkippableSliceDuration;
+  return js::SliceBudget(budgetTime);
+}
+
 }  // namespace mozilla
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -285,17 +285,16 @@ UNIFIED_SOURCES += [
     "Attr.cpp",
     "AttrArray.cpp",
     "BarProps.cpp",
     "BindContext.cpp",
     "BodyConsumer.cpp",
     "BodyStream.cpp",
     "BodyUtil.cpp",
     "BorrowedAttrInfo.cpp",
-    "CCGCScheduler.cpp",
     "CharacterData.cpp",
     "ChildIterator.cpp",
     "ChromeMessageBroadcaster.cpp",
     "ChromeMessageSender.cpp",
     "ChromeNodeList.cpp",
     "ChromeUtils.cpp",
     "Comment.cpp",
     "ContentFrameMessageManager.cpp",
--- a/dom/base/nsJSEnvironment.cpp
+++ b/dom/base/nsJSEnvironment.cpp
@@ -74,41 +74,56 @@
 #include "mozilla/Telemetry.h"
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/dom/CanvasRenderingContext2DBinding.h"
 #include "mozilla/ContentEvents.h"
 #include "mozilla/CycleCollectedJSContext.h"
 #include "nsCycleCollectionNoteRootCallback.h"
 #include "mozilla/IdleTaskRunner.h"
+#include "mozilla/ipc/IdleSchedulerChild.h"
 #include "nsViewManager.h"
 #include "mozilla/EventStateManager.h"
 #include "mozilla/ProfilerLabels.h"
 #include "mozilla/ProfilerMarkers.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 // Thank you Microsoft!
 #ifdef CompareString
 #  undef CompareString
 #endif
 
+static nsITimer* sShrinkingGCTimer;
+static StaticRefPtr<IdleTaskRunner> sCCRunner;
+static nsITimer* sFullGCTimer;
+static StaticRefPtr<IdleTaskRunner> sGCRunner;
+
 static JS::GCSliceCallback sPrevGCSliceCallback;
 
 static bool sIncrementalCC = false;
 
 static bool sIsInitialized;
 static bool sShuttingDown;
 
+// nsJSEnvironmentObserver observes the user-interaction-inactive notifications
+// and triggers a shrinking a garbage collection if the user is still inactive
+// after NS_SHRINKING_GC_DELAY ms later, if the appropriate pref is set.
+
+static bool sIsCompactingOnUserInactive = false;
+static bool sUserIsActive = true;
+
+static TimeDuration sGCUnnotifiedTotalTime;
+
 static CCGCScheduler sScheduler;
 
-TimeStamp mozilla::CCGCScheduler::Now() { return TimeStamp::Now(); }
-
-uint32_t mozilla::CCGCScheduler::SuspectedCCObjects() {
+inline TimeStamp mozilla::CCGCScheduler::Now() { return TimeStamp::Now(); }
+
+inline uint32_t mozilla::CCGCScheduler::SuspectedCCObjects() {
   return nsCycleCollector_suspectedCount();
 }
 
 struct CycleCollectorStats {
   constexpr CycleCollectorStats() = default;
   void Init();
   void Clear();
   void AfterPrepareForCycleCollectionSlice(TimeStamp aDeadline,
@@ -265,16 +280,23 @@ static TimeDuration GetCollectionTimeDel
   TimeStamp now = TimeStamp::Now();
   if (sFirstCollectionTime) {
     return now - sFirstCollectionTime;
   }
   sFirstCollectionTime = now;
   return TimeDuration();
 }
 
+static void KillTimers() {
+  nsJSContext::KillShrinkingGCTimer();
+  nsJSContext::KillCCRunner();
+  nsJSContext::KillFullGCTimer();
+  nsJSContext::KillGCRunner();
+}
+
 class nsJSEnvironmentObserver final : public nsIObserver {
   ~nsJSEnvironmentObserver() = default;
 
  public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIOBSERVER
 };
 
@@ -304,24 +326,34 @@ nsJSEnvironmentObserver::Observe(nsISupp
         nsJSContext::SetLowMemoryState(true);
       }
       // Asynchronously GC.
       nsJSContext::LowMemoryGC();
     }
   } else if (!nsCRT::strcmp(aTopic, "memory-pressure-stop")) {
     nsJSContext::SetLowMemoryState(false);
   } else if (!nsCRT::strcmp(aTopic, "user-interaction-inactive")) {
-    sScheduler.UserIsInactive();
+    sUserIsActive = false;
+    if (StaticPrefs::javascript_options_compact_on_user_inactive()) {
+      nsJSContext::PokeShrinkingGC();
+    }
   } else if (!nsCRT::strcmp(aTopic, "user-interaction-active")) {
-    sScheduler.UserIsActive();
+    sUserIsActive = true;
+    nsJSContext::KillShrinkingGCTimer();
+    if (sIsCompactingOnUserInactive) {
+      AutoJSAPI jsapi;
+      jsapi.Init();
+      JS::AbortIncrementalGC(jsapi.cx());
+    }
+    MOZ_ASSERT(!sIsCompactingOnUserInactive);
   } else if (!nsCRT::strcmp(aTopic, "quit-application") ||
              !nsCRT::strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) ||
              !nsCRT::strcmp(aTopic, "content-child-will-shutdown")) {
     sShuttingDown = true;
-    sScheduler.Shutdown();
+    KillTimers();
   }
 
   return NS_OK;
 }
 
 /****************************************************************
  ************************** AutoFree ****************************
  ****************************************************************/
@@ -540,17 +572,17 @@ nsJSContext::nsJSContext(bool aGCOnDestr
 nsJSContext::~nsJSContext() {
   mGlobalObjectRef = nullptr;
 
   Destroy();
 }
 
 void nsJSContext::Destroy() {
   if (mGCOnDestruction) {
-    sScheduler.PokeGC(JS::GCReason::NSJSCONTEXT_DESTROY, mWindowProxy);
+    PokeGC(JS::GCReason::NSJSCONTEXT_DESTROY, mWindowProxy);
   }
 
   DropJSObjects(this);
 }
 
 // QueryInterface implementation for nsJSContext
 NS_IMPL_CYCLE_COLLECTION_CLASS(nsJSContext)
 
@@ -1019,16 +1051,69 @@ nsresult nsJSContext::InitClasses(JS::Ha
 }
 
 bool nsJSContext::GetProcessingScriptTag() { return mProcessingScriptTag; }
 
 void nsJSContext::SetProcessingScriptTag(bool aFlag) {
   mProcessingScriptTag = aFlag;
 }
 
+using MayGCPromise =
+    MozPromise<bool /* aIgnored */, mozilla::ipc::ResponseRejectReason, true>;
+
+// Returns null if we shouldn't GC now (eg a GC is already running).
+static RefPtr<MayGCPromise> MayGCNow(JS::GCReason reason) {
+  using namespace mozilla::ipc;
+
+  // We ask the parent if we should GC for GCs that aren't too timely, with the
+  // exception of MEM_PRESSURE, in that case we ask the parent because GCing on
+  // too many processes at the same time when under memory pressure could be a
+  // very bad experience for the user.
+  switch (reason) {
+    case JS::GCReason::PAGE_HIDE:
+    case JS::GCReason::MEM_PRESSURE:
+    case JS::GCReason::USER_INACTIVE:
+    case JS::GCReason::FULL_GC_TIMER:
+    case JS::GCReason::CC_FINISHED: {
+      if (XRE_IsContentProcess()) {
+        IdleSchedulerChild* child =
+            IdleSchedulerChild::GetMainThreadIdleScheduler();
+        if (child) {
+          return child->MayGCNow();
+        }
+      }
+      // The parent process doesn't ask IdleSchedulerParent if it can GC.
+      // TODO: but it should ask.
+      break;
+    }
+    default:
+      break;
+  }
+
+  return MayGCPromise::CreateAndResolve(true, __func__);
+}
+
+void FullGCTimerFired(nsITimer* aTimer, void* aClosure) {
+  nsJSContext::KillFullGCTimer();
+  MOZ_ASSERT(!aClosure, "Don't pass a closure to FullGCTimerFired");
+
+  RefPtr<MayGCPromise> mbPromise = MayGCNow(JS::GCReason::FULL_GC_TIMER);
+  if (mbPromise) {
+    mbPromise->Then(
+        GetMainThreadSerialEventTarget(), __func__,
+        [](bool aIgnored) {
+          nsJSContext::GarbageCollectNow(JS::GCReason::FULL_GC_TIMER,
+                                         nsJSContext::IncrementalGC);
+        },
+        [](mozilla::ipc::ResponseRejectReason r) {
+          // do nothing
+        });
+  }
+}
+
 // static
 void nsJSContext::SetLowMemoryState(bool aState) {
   JSContext* cx = danger::GetJSContext();
   JS::SetLowMemoryState(cx, aState);
 }
 
 // static
 void nsJSContext::GarbageCollectNow(JS::GCReason aReason,
@@ -1063,17 +1148,17 @@ void nsJSContext::GarbageCollectNow(JS::
     sScheduler.SetNeedsFullGC();
   }
 
   if (sScheduler.NeedsFullGC()) {
     JS::PrepareForFullGC(cx);
   }
 
   if (aIncremental == IncrementalGC) {
-    // Incremental GC slices will be triggered by the GC Runner. If one doesn't
+    // Incremental GC slices will be triggered by the sGCRunner. If one doesn't
     // already exist, create it in the GC_SLICE_END callback for the first
     // slice being executed here.
     JS::StartIncrementalGC(cx, options, aReason, aSliceMillis);
   } else {
     JS::NonIncrementalGC(cx, options, aReason);
   }
 }
 
@@ -1459,45 +1544,46 @@ void nsJSContext::BeginCycleCollectionCa
     }
     sCCStats.AfterSyncForgetSkippable(startTime);
   }
 
   if (sShuttingDown) {
     return;
   }
 
-  sScheduler.InitCCRunnerStateMachine(
-      mozilla::CCGCScheduler::CCRunnerState::CycleCollecting);
-  sScheduler.EnsureCCRunner(kICCIntersliceDelay, kIdleICCSliceBudget);
+  if (!sCCRunner) {
+    sScheduler.InitCCRunnerStateMachine(
+        mozilla::CCGCScheduler::CCRunnerState::CycleCollecting);
+  }
+  EnsureCCRunner(kICCIntersliceDelay, kIdleICCSliceBudget);
 }
 
 // static
 void nsJSContext::EndCycleCollectionCallback(CycleCollectorResults& aResults) {
   MOZ_ASSERT(NS_IsMainThread());
 
-  sScheduler.KillCCRunner();
+  nsJSContext::KillCCRunner();
 
   // Update timing information for the current slice before we log it, if
   // we previously called PrepareForCycleCollectionSlice(). During shutdown
   // CCs, this won't happen.
   sCCStats.AfterCycleCollectionSlice();
   sScheduler.NoteCycleCollected(aResults);
 
   TimeStamp endCCTimeStamp = TimeStamp::Now();
   TimeDuration ccNowDuration = TimeBetween(sCCStats.mBeginTime, endCCTimeStamp);
 
   if (sScheduler.NeedsGCAfterCC()) {
     MOZ_ASSERT(StaticPrefs::javascript_options_gc_delay() >
                    kMaxICCDuration.ToMilliseconds(),
                "A max duration ICC shouldn't reduce GC delay to 0");
 
-    sScheduler.PokeGC(
-        JS::GCReason::CC_FINISHED, nullptr,
-        StaticPrefs::javascript_options_gc_delay() -
-            std::min(ccNowDuration, kMaxICCDuration).ToMilliseconds());
+    PokeGC(JS::GCReason::CC_FINISHED, nullptr,
+           StaticPrefs::javascript_options_gc_delay() -
+               std::min(ccNowDuration, kMaxICCDuration).ToMilliseconds());
   }
 
   // Log information about the CC via telemetry, JSON and the console.
 
   sCCStats.SendTelemetry(ccNowDuration);
 
   uint32_t cleanups = std::max(sCCStats.mForgetSkippableBeforeCC, 1u);
 
@@ -1505,18 +1591,131 @@ void nsJSContext::EndCycleCollectionCall
 
   sCCStats.MaybeNotifyStats(aResults, ccNowDuration, cleanups);
 
   // Update global state to indicate we have just run a cycle collection.
   sScheduler.NoteCCEnd(endCCTimeStamp);
   sCCStats.Clear();
 }
 
-/* static */
-bool CCGCScheduler::CCRunnerFired(TimeStamp aDeadline) {
+// static
+bool GCRunnerFired(TimeStamp aDeadline, void* aClosure) {
+  MOZ_ASSERT(!aClosure, "Don't pass a closure to GCRunnerFired");
+  MOZ_ASSERT(!sShuttingDown, "GCRunner still alive during shutdown");
+
+  GCRunnerStep step = sScheduler.GetNextGCRunnerAction(aDeadline);
+  switch (step.mAction) {
+    case GCRunnerAction::None:
+      MOZ_CRASH("Unexpected GCRunnerAction");
+
+    case GCRunnerAction::WaitToMajorGC: {
+      RefPtr<MayGCPromise> mbPromise = MayGCNow(step.mReason);
+      if (!mbPromise || mbPromise->IsResolved()) {
+        // Only use the promise if it's not resolved yet, otherwise fall through
+        // and begin the GC in the current idle time with our current deadline.
+        break;
+      }
+
+      nsJSContext::KillGCRunner();
+      mbPromise->Then(
+          GetMainThreadSerialEventTarget(), __func__,
+          [](bool aIgnored) {
+            if (!sScheduler.NoteReadyForMajorGC()) {
+              return;  // Another GC completed while waiting.
+            }
+            // If a new runner was started, recreate it with a 0 delay. The new
+            // runner will continue in idle time.
+            nsJSContext::KillGCRunner();
+            sGCRunner = IdleTaskRunner::Create(
+                [](TimeStamp aDeadline) {
+                  return GCRunnerFired(aDeadline, nullptr);
+                },
+                "GCRunnerFired", 0,
+                StaticPrefs::javascript_options_gc_delay_interslice(),
+                int64_t(sScheduler.mActiveIntersliceGCBudget.ToMilliseconds()),
+                true, [] { return sShuttingDown; });
+          },
+          [](mozilla::ipc::ResponseRejectReason r) {});
+
+      return true;
+    }
+
+    case GCRunnerAction::StartMajorGC:
+    case GCRunnerAction::GCSlice:
+      break;
+  }
+
+  // Run a GC slice, possibly the first one of a major GC.
+
+  MOZ_ASSERT(sScheduler.mActiveIntersliceGCBudget);
+  TimeStamp startTimeStamp = TimeStamp::Now();
+  TimeDuration budget =
+      sScheduler.ComputeInterSliceGCBudget(aDeadline, startTimeStamp);
+  TimeDuration duration = sGCUnnotifiedTotalTime;
+  nsJSContext::GarbageCollectNow(step.mReason, nsJSContext::IncrementalGC,
+                                 nsJSContext::NonShrinkingGC,
+                                 budget.ToMilliseconds());
+
+  sGCUnnotifiedTotalTime = TimeDuration();
+  TimeStamp now = TimeStamp::Now();
+  TimeDuration sliceDuration = now - startTimeStamp;
+  duration += sliceDuration;
+  if (duration.ToSeconds()) {
+    TimeDuration idleDuration;
+    if (!aDeadline.IsNull()) {
+      if (aDeadline < now) {
+        // This slice overflowed the idle period.
+        idleDuration = aDeadline - startTimeStamp;
+      } else {
+        // Note, we don't want to use duration here, since it may contain
+        // data also from JS engine triggered GC slices.
+        idleDuration = sliceDuration;
+      }
+    }
+
+    uint32_t percent =
+        uint32_t(idleDuration.ToSeconds() / duration.ToSeconds() * 100);
+    Telemetry::Accumulate(Telemetry::GC_SLICE_DURING_IDLE, percent);
+  }
+
+  // If the GC doesn't have any more work to do on the foreground thread (and
+  // e.g. is waiting for background sweeping to finish) then return false to
+  // make IdleTaskRunner postpone the next call a bit.
+  JSContext* cx = danger::GetJSContext();
+  return JS::IncrementalGCHasForegroundWork(cx);
+}
+
+// static
+void ShrinkingGCTimerFired(nsITimer* aTimer, void* aClosure) {
+  nsJSContext::KillShrinkingGCTimer();
+
+  RefPtr<MayGCPromise> mbPromise = MayGCNow(JS::GCReason::USER_INACTIVE);
+  if (mbPromise) {
+    mbPromise->Then(
+        GetMainThreadSerialEventTarget(), __func__,
+        [](bool aIgnored) {
+          if (!sUserIsActive) {
+            sIsCompactingOnUserInactive = true;
+            nsJSContext::GarbageCollectNow(JS::GCReason::USER_INACTIVE,
+                                           nsJSContext::IncrementalGC,
+                                           nsJSContext::ShrinkingGC);
+          } else {
+            using mozilla::ipc::IdleSchedulerChild;
+            IdleSchedulerChild* child =
+                IdleSchedulerChild::GetMainThreadIdleScheduler();
+            if (child) {
+              child->DoneGC();
+            }
+          }
+        },
+        [](mozilla::ipc::ResponseRejectReason r) {});
+  }
+}
+
+static bool CCRunnerFired(TimeStamp aDeadline) {
   bool didDoWork = false;
 
   // The CC/GC scheduler (sScheduler) decides what action(s) to take during
   // this invocation of the CC runner.
   //
   // This may be zero, one, or multiple actions. (Zero is when CC is blocked by
   // incremental GC, or when the scheduler determined that a CC is no longer
   // needed.) Loop until the scheduler finishes this invocation by returning
@@ -1546,37 +1745,78 @@ bool CCGCScheduler::CCRunnerFired(TimeSt
       case CCRunnerAction::CycleCollect:
         // Cycle collection slice.
         nsJSContext::RunCycleCollectorSlice(aDeadline);
         break;
 
       case CCRunnerAction::StopRunning:
         // End this CC, either because we have run a cycle collection slice, or
         // because a CC is no longer needed.
-        sScheduler.KillCCRunner();
+        nsJSContext::KillCCRunner();
         break;
     }
 
     if (step.mAction != CCRunnerAction::None) {
       didDoWork = true;
     }
   } while (step.mYield == CCRunnerYield::Continue);
 
   return didDoWork;
 }
 
 // static
+void nsJSContext::EnsureCCRunner(TimeDuration aDelay, TimeDuration aBudget) {
+  MOZ_ASSERT(!sShuttingDown);
+
+  if (!sCCRunner) {
+    sCCRunner = IdleTaskRunner::Create(
+        CCRunnerFired, "EnsureCCRunner::CCRunnerFired", 0,
+        aDelay.ToMilliseconds(), aBudget.ToMilliseconds(), true,
+        [] { return sShuttingDown; });
+  } else {
+    sCCRunner->SetMinimumUsefulBudget(aBudget.ToMilliseconds());
+    nsIEventTarget* target = mozilla::GetCurrentEventTarget();
+    if (target) {
+      sCCRunner->SetTimer(aDelay.ToMilliseconds(), target);
+    }
+  }
+}
+
+// static
 bool nsJSContext::HasHadCleanupSinceLastGC() {
   return sScheduler.IsEarlyForgetSkippable(1);
 }
 
+// Check all of the various collector timers/runners and see if they are waiting
+// to fire. This does not check sFullGCTimer, as that's a more expensive
+// collection we run on a long timer.
+
 // static
 void nsJSContext::RunNextCollectorTimer(JS::GCReason aReason,
                                         mozilla::TimeStamp aDeadline) {
-  sScheduler.RunNextCollectorTimer(aReason, aDeadline);
+  if (sShuttingDown) {
+    return;
+  }
+
+  // When we're in an incremental GC, we should always have an sGCRunner, so do
+  // not check CC timers. The CC timers won't do anything during a GC.
+  MOZ_ASSERT_IF(sScheduler.InIncrementalGC(), sGCRunner);
+
+  RefPtr<IdleTaskRunner> runner;
+  if (sGCRunner) {
+    sScheduler.SetWantMajorGC(aReason);
+    runner = sGCRunner;
+  } else if (sCCRunner) {
+    runner = sCCRunner;
+  }
+
+  if (runner) {
+    runner->SetIdleDeadline(aDeadline);
+    runner->Run();
+  }
 }
 
 // static
 void nsJSContext::MaybeRunNextCollectorSlice(nsIDocShell* aDocShell,
                                              JS::GCReason aReason) {
   if (!aDocShell || !XRE_IsContentProcess()) {
     return;
   }
@@ -1622,25 +1862,78 @@ void nsJSContext::MaybeRunNextCollectorS
   // the page for awhile.
   if ((currentTime - lastEventTime) >
       (StaticPrefs::dom_events_user_interaction_interval() *
        PR_USEC_PER_MSEC)) {
     Maybe<TimeStamp> next = nsRefreshDriver::GetNextTickHint();
     // Try to not delay the next RefreshDriver tick, so give a reasonable
     // deadline for collectors.
     if (next.isSome()) {
-      sScheduler.RunNextCollectorTimer(aReason, next.value());
+      nsJSContext::RunNextCollectorTimer(aReason, next.value());
     }
   }
 }
 
 // static
 void nsJSContext::PokeGC(JS::GCReason aReason, JSObject* aObj,
                          uint32_t aDelay) {
-  sScheduler.PokeGC(aReason, aObj, aDelay);
+  if (sShuttingDown) {
+    return;
+  }
+
+  if (aObj) {
+    JS::Zone* zone = JS::GetObjectZone(aObj);
+    CycleCollectedJSRuntime::Get()->AddZoneWaitingForGC(zone);
+  } else if (aReason != JS::GCReason::CC_FINISHED) {
+    sScheduler.SetNeedsFullGC();
+  }
+
+  if (sGCRunner) {
+    // There's already a runner for GC'ing, just return
+    return;
+  }
+
+  sScheduler.SetWantMajorGC(aReason);
+
+  if (sCCRunner) {
+    // Make sure CC is called regardless of the size of the purple buffer, and
+    // GC after it.
+    sScheduler.EnsureCCThenGC();
+    return;
+  }
+
+  static bool first = true;
+
+  uint32_t delay =
+      aDelay ? aDelay
+             : (first ? StaticPrefs::javascript_options_gc_delay_first()
+                      : StaticPrefs::javascript_options_gc_delay());
+  first = false;
+
+  sGCRunner = IdleTaskRunner::Create(
+      [](TimeStamp aDeadline) { return GCRunnerFired(aDeadline, nullptr); },
+      "GCRunnerFired",
+      // Wait for javascript.options.gc_delay, then start looking for idle time
+      // to run the initial GC slice. Wait at most the interslice GC delay
+      // before forcing a run.
+      delay, StaticPrefs::javascript_options_gc_delay_interslice(),
+      sScheduler.mActiveIntersliceGCBudget.ToMilliseconds(), true,
+      [] { return sShuttingDown; });
+}
+
+// static
+void nsJSContext::PokeShrinkingGC() {
+  if (sShrinkingGCTimer || sShuttingDown) {
+    return;
+  }
+
+  NS_NewTimerWithFuncCallback(
+      &sShrinkingGCTimer, ShrinkingGCTimerFired, nullptr,
+      StaticPrefs::javascript_options_compact_on_user_inactive_delay(),
+      nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY, "ShrinkingGCTimerFired");
 }
 
 // static
 void nsJSContext::DoLowMemoryGC() {
   if (sShuttingDown) {
     return;
   }
   nsJSContext::GarbageCollectNow(JS::GCReason::MEM_PRESSURE,
@@ -1651,31 +1944,77 @@ void nsJSContext::DoLowMemoryGC() {
     nsJSContext::GarbageCollectNow(JS::GCReason::MEM_PRESSURE,
                                    nsJSContext::NonIncrementalGC,
                                    nsJSContext::ShrinkingGC);
   }
 }
 
 // static
 void nsJSContext::LowMemoryGC() {
-  RefPtr<CCGCScheduler::MayGCPromise> mbPromise =
-      CCGCScheduler::MayGCNow(JS::GCReason::MEM_PRESSURE);
+  RefPtr<MayGCPromise> mbPromise = MayGCNow(JS::GCReason::MEM_PRESSURE);
   if (!mbPromise) {
     // Normally when the promise is null it means that IPC failed, that probably
     // means that something bad happened, don't bother with the GC.
     return;
   }
   mbPromise->Then(
       GetMainThreadSerialEventTarget(), __func__,
       [](bool aIgnored) { DoLowMemoryGC(); },
       [](mozilla::ipc::ResponseRejectReason r) {});
 }
 
 // static
-void nsJSContext::MaybePokeCC() { sScheduler.MaybePokeCC(); }
+void nsJSContext::MaybePokeCC() {
+  if (sCCRunner || sShuttingDown) {
+    return;
+  }
+
+  if (sScheduler.ShouldScheduleCC()) {
+    // We can kill some objects before running forgetSkippable.
+    nsCycleCollector_dispatchDeferredDeletion();
+
+    if (!sCCRunner) {
+      sScheduler.InitCCRunnerStateMachine(
+          mozilla::CCGCScheduler::CCRunnerState::ReducePurple);
+    }
+    EnsureCCRunner(kCCSkippableDelay, kForgetSkippableSliceDuration);
+  }
+}
+
+void nsJSContext::KillFullGCTimer() {
+  if (sFullGCTimer) {
+    sFullGCTimer->Cancel();
+    NS_RELEASE(sFullGCTimer);
+  }
+}
+
+void nsJSContext::KillGCRunner() {
+  if (sGCRunner) {
+    sGCRunner->Cancel();
+    sGCRunner = nullptr;
+  }
+}
+
+// static
+void nsJSContext::KillShrinkingGCTimer() {
+  if (sShrinkingGCTimer) {
+    sShrinkingGCTimer->Cancel();
+    NS_RELEASE(sShrinkingGCTimer);
+  }
+}
+
+// static
+void nsJSContext::KillCCRunner() {
+  sScheduler.UnblockCC();
+  sScheduler.DeactivateCCRunner();
+  if (sCCRunner) {
+    sCCRunner->Cancel();
+    sCCRunner = nullptr;
+  }
+}
 
 static void DOMGCSliceCallback(JSContext* aCx, JS::GCProgress aProgress,
                                const JS::GCDescription& aDesc) {
   NS_ASSERTION(NS_IsMainThread(), "GCs must run on the main thread");
 
   static TimeStamp sCurrentGCStartTime;
 
   switch (aProgress) {
@@ -1700,59 +2039,76 @@ static void DOMGCSliceCallback(JSContext
         nsCOMPtr<nsIConsoleService> cs =
             do_GetService(NS_CONSOLESERVICE_CONTRACTID);
         if (cs) {
           cs->LogStringMessage(msg.get());
         }
       }
 
       sScheduler.NoteGCEnd();
+      sIsCompactingOnUserInactive = false;
 
       using mozilla::ipc::IdleSchedulerChild;
       IdleSchedulerChild* child =
           IdleSchedulerChild::GetMainThreadIdleScheduler();
       if (child) {
         child->DoneGC();
       }
 
       // May need to kill the GC runner
-      sScheduler.KillGCRunner();
-
-      sScheduler.MaybePokeCC();
+      nsJSContext::KillGCRunner();
+
+      nsJSContext::MaybePokeCC();
 
       if (aDesc.isZone_) {
-        sScheduler.PokeFullGC();
+        if (!sFullGCTimer && !sShuttingDown) {
+          NS_NewTimerWithFuncCallback(
+              &sFullGCTimer, FullGCTimerFired, nullptr,
+              StaticPrefs::javascript_options_gc_delay_full(),
+              nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY, "FullGCTimerFired");
+        }
       } else {
+        nsJSContext::KillFullGCTimer();
         sScheduler.SetNeedsFullGC(false);
-        sScheduler.KillFullGCTimer();
       }
 
       if (sScheduler.IsCCNeeded()) {
         nsCycleCollector_dispatchDeferredDeletion();
       }
 
       Telemetry::Accumulate(Telemetry::GC_IN_PROGRESS_MS,
                             TimeUntilNow(sCurrentGCStartTime).ToMilliseconds());
       break;
     }
 
     case JS::GC_SLICE_BEGIN:
       break;
 
     case JS::GC_SLICE_END:
-      sScheduler.NoteGCSliceEnd(aDesc.lastSliceEnd(aCx) -
-                                aDesc.lastSliceStart(aCx));
+      sScheduler.NoteGCSliceEnd();
+
+      sGCUnnotifiedTotalTime +=
+          aDesc.lastSliceEnd(aCx) - aDesc.lastSliceStart(aCx);
 
       if (sShuttingDown || aDesc.isComplete_) {
-        sScheduler.KillGCRunner();
-      } else {
-        // If incremental GC wasn't triggered by GCTimerFired, we may not have a
-        // runner to ensure all the slices are handled. So, create the runner
-        // here.
-        sScheduler.EnsureGCRunner(0);
+        nsJSContext::KillGCRunner();
+      } else if (!sGCRunner) {
+        // If incremental GC wasn't triggered by GCTimerFired, we may not
+        // have a runner to ensure all the slices are handled. So, create
+        // the runner here.
+        sGCRunner = IdleTaskRunner::Create(
+            [](TimeStamp aDeadline) {
+              return GCRunnerFired(aDeadline, nullptr);
+            },
+            "DOMGCSliceCallback::GCRunnerFired",
+            // Start immediately looking for idle time, waiting at most the
+            // interslice GC delay before forcing a run.
+            0, StaticPrefs::javascript_options_gc_delay_interslice(),
+            sScheduler.mActiveIntersliceGCBudget.ToMilliseconds(), true,
+            [] { return sShuttingDown; });
       }
 
       if (sScheduler.IsCCNeeded()) {
         nsCycleCollector_dispatchDeferredDeletion();
       }
 
       if (StaticPrefs::javascript_options_mem_log()) {
         nsString gcstats;
@@ -1786,16 +2142,17 @@ void nsJSContext::SetWindowProxy(JS::Han
 JSObject* nsJSContext::GetWindowProxy() { return mWindowProxy; }
 
 void nsJSContext::LikelyShortLivingObjectCreated() {
   ++sScheduler.mLikelyShortLivingObjectsNeedingGC;
 }
 
 void mozilla::dom::StartupJSEnvironment() {
   // initialize all our statics, so that we can restart XPCOM
+  sShrinkingGCTimer = sFullGCTimer = nullptr;
   sIsInitialized = false;
   sShuttingDown = false;
   new (&sScheduler) CCGCScheduler();  // Reset the scheduler state.
   sCCStats.Init();
 }
 
 static void SetGCParameter(JSGCParamKey aParam, uint32_t aValue) {
   AutoJSAPI jsapi;
@@ -2057,16 +2414,18 @@ void nsJSContext::EnsureStatics() {
   obs->AddObserver(observer, "quit-application", false);
   obs->AddObserver(observer, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
   obs->AddObserver(observer, "content-child-will-shutdown", false);
 
   sIsInitialized = true;
 }
 
 void mozilla::dom::ShutdownJSEnvironment() {
+  KillTimers();
+
   sShuttingDown = true;
   sScheduler.Shutdown();
 }
 
 AsyncErrorReporter::AsyncErrorReporter(xpc::ErrorReport* aReport)
     : Runnable("dom::AsyncErrorReporter"), mReport(aReport) {}
 
 void AsyncErrorReporter::SerializeStack(JSContext* aCx,
--- a/dom/base/nsJSEnvironment.h
+++ b/dom/base/nsJSEnvironment.h
@@ -97,25 +97,34 @@ class nsJSContext : public nsIScriptCont
   // If user has been idle and aDocShell is for an iframe being loaded in an
   // already loaded top level docshell, this will run a CC or GC
   // timer/runner if there is such pending.
   static void MaybeRunNextCollectorSlice(nsIDocShell* aDocShell,
                                          JS::GCReason aReason);
 
   // The GC should probably run soon, in the zone of object aObj (if given).
   static void PokeGC(JS::GCReason aReason, JSObject* aObj, uint32_t aDelay = 0);
+  static void KillGCTimer();
+
+  static void PokeShrinkingGC();
+  static void KillShrinkingGCTimer();
 
   // Immediately perform a non-incremental shrinking GC and CC.
   static void DoLowMemoryGC();
 
   // Perform a non-incremental shrinking GC and CC according to
   // IdleScheduler.
   static void LowMemoryGC();
 
   static void MaybePokeCC();
+  static void EnsureCCRunner(mozilla::TimeDuration aDelay,
+                             mozilla::TimeDuration aBudget);
+  static void KillCCRunner();
+  static void KillFullGCTimer();
+  static void KillGCRunner();
 
   // Calling LikelyShortLivingObjectCreated() makes a GC more likely.
   static void LikelyShortLivingObjectCreated();
 
   static bool HasHadCleanupSinceLastGC();
 
   nsIScriptGlobalObject* GetCachedGlobalObject() {
     // Verify that we have a global so that this