tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

CCGCScheduler.cpp (37137B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 #include "CCGCScheduler.h"
      6 
      7 #include "js/GCAPI.h"
      8 #include "mozilla/CycleCollectedJSRuntime.h"
      9 #include "mozilla/PerfStats.h"
     10 #include "mozilla/ProfilerMarkers.h"
     11 #include "mozilla/StaticPrefs_javascript.h"
     12 #include "mozilla/dom/ScriptSettings.h"
     13 #include "mozilla/glean/DomMetrics.h"
     14 #include "nsRefreshDriver.h"
     15 
     16 /* Globally initialized constants
     17 */
     18 namespace mozilla {
     19 MOZ_RUNINIT const TimeDuration kOneMinute = TimeDuration::FromSeconds(60.0f);
     20 MOZ_RUNINIT const TimeDuration kCCDelay = TimeDuration::FromSeconds(6);
     21 MOZ_RUNINIT const TimeDuration kCCSkippableDelay =
     22    TimeDuration::FromMilliseconds(250);
     23 MOZ_RUNINIT const TimeDuration kTimeBetweenForgetSkippableCycles =
     24    TimeDuration::FromSeconds(2);
     25 MOZ_RUNINIT const TimeDuration kForgetSkippableSliceDuration =
     26    TimeDuration::FromMilliseconds(2);
     27 MOZ_RUNINIT const TimeDuration kICCIntersliceDelay =
     28    TimeDuration::FromMilliseconds(250);
     29 MOZ_RUNINIT const TimeDuration kICCSliceBudget =
     30    TimeDuration::FromMilliseconds(3);
     31 MOZ_RUNINIT const TimeDuration kIdleICCSliceBudget =
     32    TimeDuration::FromMilliseconds(2);
     33 MOZ_RUNINIT const TimeDuration kMaxICCDuration = TimeDuration::FromSeconds(2);
     34 
     35 MOZ_RUNINIT const TimeDuration kCCForced = kOneMinute * 2;
     36 MOZ_RUNINIT const TimeDuration kMaxCCLockedoutTime =
     37    TimeDuration::FromSeconds(30);
     38 }  // namespace mozilla
     39 
     40 /*
     41 * GC Scheduling from Firefox
     42 * ==========================
     43 *
     44 * See also GC Scheduling from SpiderMonkey's perspective here:
     45 * https://searchfox.org/mozilla-central/source/js/src/gc/Scheduling.h
     46 *
     47 * From Firefox's perspective GCs can start in 5 different ways:
     48 *
     49 *  * The JS engine just starts doing a GC for its own reasons (see above).
     50 *    Firefox finds out about these via a callback in nsJSEnvironment.cpp
     51 *  * PokeGC()
     52 *  * PokeFullGC()
     53 *  * PokeShrinkingGC()
     54 *  * memory-pressure GCs (via a listener in nsJSEnvironment.cpp).
     55 *
     56 * PokeGC
     57 * ------
     58 *
     59 * void CCGCScheduler::PokeGC(JS::GCReason aReason, JSObject* aObj,
     60 *                            TimeDuration aDelay)
     61 *
     62 * PokeGC provides a way for callers to say "Hey, there may be some memory
     63 * associated with this object (via Zone) you can collect."  PokeGC will:
     64 *  * add the zone to a set,
     65 *  * set flags including what kind of GC to run (SetWantMajorGC),
     66 *  * then creates the mGCRunner with a short delay.
     67 *
     68 * The delay can allow other calls to PokeGC to add their zones so they can
     69 * be collected together.
     70 *
     71 * See below for what happens when mGCRunner fires.
     72 *
     73 * PokeFullGC
     74 * ----------
     75 *
     76 * void CCGCScheduler::PokeFullGC()
     77 *
     78 * PokeFullGC will create a timer that will initiate a "full" (all zones)
     79 * collection.  This is usually used after a regular collection if a full GC
     80 * seems like a good idea (to collect inter-zone references).
     81 *
     82 * When the timer fires it will:
     83 *  * set flags (SetWantMajorGC),
     84 *  * start the mGCRunner with zero delay.
     85 *
     86 * See below for when mGCRunner fires.
     87 *
     88 * PokeShrinkingGC
     89 * ---------------
     90 *
     91 * void CCGCScheduler::PokeShrinkingGC()
     92 *
     93 * PokeShrinkingGC is called when Firefox's user is inactive.
     94 * Like PokeFullGC, PokeShrinkingGC uses a timer, but the timeout is longer
     95 * which should prevent the ShrinkingGC from starting if the user only
     96 * glances away for a brief time.  When the timer fires it will:
     97 *
     98 *  * set flags (SetWantMajorGC),
     99 *  * create the mGCRunner.
    100 *
    101 * There is a check if the user is still inactive in GCRunnerFired), if the
    102 * user has become active the shrinking GC is canceled and either a regular
    103 * GC (if requested, see mWantAtLeastRegularGC) or no GC is run.
    104 *
    105 * When mGCRunner fires
    106 * --------------------
    107 *
    108 * When mGCRunner fires it calls GCRunnerFired.  This starts in the
    109 * WaitToMajorGC state:
    110 *
    111 *  * If this is a parent process it jumps to the next state
    112 *  * If this is a content process it will ask the parent if now is a good
    113 *      time to do a GC.  (MayGCNow)
    114 *  * kill the mGCRunner
    115 *  * Exit
    116 *
    117 * Meanwhile the parent process will queue GC requests so that not too many
    118 * are running in parallel overwhelming the CPU cores (see
    119 * IdleSchedulerParent).
    120 *
    121 * When the promise from MayGCNow is resolved it will set some
    122 * state (NoteReadyForMajorGC) and restore the mGCRunner.
    123 *
    124 * When the mGCRunner runs a second time (or this is the parent process and
    125 * which jumped over the above logic.  It will be in the StartMajorGC state.
    126 * It will initiate the GC for real, usually.  If it's a shrinking GC and the
    127 * user is now active again it may abort.  See GCRunnerFiredDoGC().
    128 *
    129 * The runner will then run the first slice of the garbage collection.
    130 * Later slices are also run by the runner, the final slice kills the runner
    131 * from the GC callback in nsJSEnvironment.cpp.
    132 *
    133 * There is additional logic in the code to handle concurrent requests of
    134 * various kinds.
    135 */
    136 
    137 namespace mozilla {
    138 
    139 void CCGCScheduler::NoteGCBegin(JS::GCReason aReason) {
    140  // Treat all GC as incremental here; non-incremental GC will just appear to
    141  // be one slice.
    142  mInIncrementalGC = true;
    143  mReadyForMajorGC = !mAskParentBeforeMajorGC;
    144 
    145  // Tell the parent process that we've started a GC (it might not know if
    146  // we hit a threshold in the JS engine).
    147  using mozilla::ipc::IdleSchedulerChild;
    148  IdleSchedulerChild* child = IdleSchedulerChild::GetMainThreadIdleScheduler();
    149  if (child) {
    150    child->StartedGC();
    151  }
    152 
    153  // The reason might have come from mMajorReason, mEagerMajorGCReason, or
    154  // in the case of an internally-generated GC, it might come from the
    155  // internal logic (and be passed in here). It's easier to manage a single
    156  // reason state variable, so merge all sources into mMajorGCReason.
    157  MOZ_ASSERT(aReason != JS::GCReason::NO_REASON);
    158  mMajorGCReason = aReason;
    159  mEagerMajorGCReason = JS::GCReason::NO_REASON;
    160 }
    161 
    162 void CCGCScheduler::NoteGCEnd() {
    163  mMajorGCReason = JS::GCReason::NO_REASON;
    164  mEagerMajorGCReason = JS::GCReason::NO_REASON;
    165  mEagerMinorGCReason = JS::GCReason::NO_REASON;
    166 
    167  mInIncrementalGC = false;
    168  mCCBlockStart = TimeStamp();
    169  mReadyForMajorGC = !mAskParentBeforeMajorGC;
    170  mWantAtLeastRegularGC = false;
    171  mNeedsFullCC = CCReason::GC_FINISHED;
    172  mHasRunGC = true;
    173  mIsCompactingOnUserInactive = false;
    174 
    175  mCleanupsSinceLastGC = 0;
    176  mCCollectedWaitingForGC = 0;
    177  mCCollectedZonesWaitingForGC = 0;
    178  mLikelyShortLivingObjectsNeedingGC = 0;
    179 
    180  using mozilla::ipc::IdleSchedulerChild;
    181  IdleSchedulerChild* child = IdleSchedulerChild::GetMainThreadIdleScheduler();
    182  if (child) {
    183    child->DoneGC();
    184  }
    185 }
    186 
    187 void CCGCScheduler::NoteGCSliceEnd(TimeStamp aStart, TimeStamp aEnd) {
    188  if (mMajorGCReason == JS::GCReason::NO_REASON) {
    189    // Internally-triggered GCs do not wait for the parent's permission to
    190    // proceed. This flag won't be checked during an incremental GC anyway,
    191    // but it better reflects reality.
    192    mReadyForMajorGC = true;
    193  }
    194 
    195  // Subsequent slices should be INTER_SLICE_GC unless they are triggered by
    196  // something else that provides its own reason.
    197  mMajorGCReason = JS::GCReason::INTER_SLICE_GC;
    198 
    199  MOZ_ASSERT(aEnd >= aStart);
    200  TimeDuration sliceDuration = aEnd - aStart;
    201  PerfStats::RecordMeasurement(PerfStats::Metric::MajorGC, sliceDuration);
    202 
    203  // Compute how much GC time was spent in predicted-to-be-idle time. In the
    204  // unlikely event that the slice started after the deadline had already
    205  // passed, treat the entire slice as non-idle.
    206  TimeDuration nonIdleDuration;
    207  bool startedIdle = mTriggeredGCDeadline.isSome() &&
    208                     !mTriggeredGCDeadline->IsNull() &&
    209                     *mTriggeredGCDeadline > aStart;
    210  if (!startedIdle) {
    211    nonIdleDuration = sliceDuration;
    212  } else {
    213    if (*mTriggeredGCDeadline < aEnd) {
    214      // Overran the idle deadline.
    215      nonIdleDuration = aEnd - *mTriggeredGCDeadline;
    216    }
    217  }
    218 
    219  PerfStats::RecordMeasurement(PerfStats::Metric::NonIdleMajorGC,
    220                               nonIdleDuration);
    221 
    222  // Note the GC_SLICE_DURING_IDLE previously had a different definition: it was
    223  // a histogram of percentages of externally-triggered slices. It is now a
    224  // histogram of percentages of all slices. That means that now you might have
    225  // a 4ms internal slice (0% during idle) followed by a 16ms external slice
    226  // (15ms during idle), whereas before this would show up as a single record of
    227  // a single slice with 75% of its time during idle (15 of 20ms).
    228  TimeDuration idleDuration = sliceDuration - nonIdleDuration;
    229  uint32_t percent =
    230      uint32_t(idleDuration.ToSeconds() / sliceDuration.ToSeconds() * 100);
    231  glean::dom::gc_slice_during_idle.AccumulateSingleSample(percent);
    232 
    233  mTriggeredGCDeadline.reset();
    234 }
    235 
    236 void CCGCScheduler::NoteCCBegin() { mIsCollectingCycles = true; }
    237 
    238 void CCGCScheduler::NoteCCEnd(const CycleCollectorResults& aResults,
    239                              TimeStamp aWhen) {
    240  mCCollectedWaitingForGC += aResults.mFreedGCed;
    241  mCCollectedZonesWaitingForGC += aResults.mFreedJSZones;
    242  mIsCollectingCycles = false;
    243  mLastCCEndTime = aWhen;
    244  mNeedsFullCC = CCReason::NO_REASON;
    245  mPreferFasterCollection =
    246      mCurrentCollectionHasSeenNonIdle &&
    247      (aResults.mFreedGCed > 10000 || aResults.mFreedRefCounted > 10000);
    248  mCurrentCollectionHasSeenNonIdle = false;
    249 }
    250 
    251 void CCGCScheduler::NoteWontGC() {
    252  mReadyForMajorGC = !mAskParentBeforeMajorGC;
    253  mMajorGCReason = JS::GCReason::NO_REASON;
    254  mEagerMajorGCReason = JS::GCReason::NO_REASON;
    255  mWantAtLeastRegularGC = false;
    256  // Don't clear the WantFullGC state, we will do a full GC the next time a
    257  // GC happens for any other reason.
    258 }
    259 
    260 bool CCGCScheduler::GCRunnerFired(TimeStamp aDeadline) {
    261  MOZ_ASSERT(!mDidShutdown, "GCRunner still alive during shutdown");
    262 
    263  if (!aDeadline) {
    264    mCurrentCollectionHasSeenNonIdle = true;
    265  } else if (mPreferFasterCollection) {
    266    // We found some idle time, try to utilize that a bit more given that
    267    // we're in a mode where idle time is rare.
    268    aDeadline = aDeadline + TimeDuration::FromMilliseconds(5.0);
    269  }
    270 
    271  GCRunnerStep step = GetNextGCRunnerAction(aDeadline);
    272  switch (step.mAction) {
    273    case GCRunnerAction::None:
    274      KillGCRunner();
    275      return false;
    276 
    277    case GCRunnerAction::MinorGC:
    278      JS::MaybeRunNurseryCollection(CycleCollectedJSRuntime::Get()->Runtime(),
    279                                    step.mReason);
    280      NoteMinorGCEnd();
    281      return HasMoreIdleGCRunnerWork();
    282 
    283    case GCRunnerAction::WaitToMajorGC: {
    284      MOZ_ASSERT(!mHaveAskedParent, "GCRunner alive after asking the parent");
    285      RefPtr<CCGCScheduler::MayGCPromise> mbPromise =
    286          CCGCScheduler::MayGCNow(step.mReason);
    287      if (!mbPromise) {
    288        // We can GC now.
    289        break;
    290      }
    291 
    292      mHaveAskedParent = true;
    293      KillGCRunner();
    294      mbPromise->Then(
    295          GetMainThreadSerialEventTarget(), __func__,
    296          [this](bool aMayGC) {
    297            mHaveAskedParent = false;
    298            if (aMayGC) {
    299              if (!NoteReadyForMajorGC()) {
    300                // Another GC started and maybe completed while waiting.
    301                return;
    302              }
    303              // Recreate the GC runner with a 0 delay.  The new runner will
    304              // continue in idle time.
    305              KillGCRunner();
    306              EnsureGCRunner(0);
    307            } else if (!InIncrementalGC()) {
    308              // We should kill the GC runner since we're done with it, but
    309              // only if there's no incremental GC.
    310              KillGCRunner();
    311              NoteWontGC();
    312            }
    313          },
    314          [this](mozilla::ipc::ResponseRejectReason r) {
    315            mHaveAskedParent = false;
    316            if (!InIncrementalGC()) {
    317              KillGCRunner();
    318              NoteWontGC();
    319            }
    320          });
    321 
    322      return true;
    323    }
    324 
    325    case GCRunnerAction::StartMajorGC:
    326    case GCRunnerAction::GCSlice:
    327      break;
    328  }
    329 
    330  return GCRunnerFiredDoGC(aDeadline, step);
    331 }
    332 
    333 bool CCGCScheduler::GCRunnerFiredDoGC(TimeStamp aDeadline,
    334                                      const GCRunnerStep& aStep) {
    335  // Run a GC slice, possibly the first one of a major GC.
    336  nsJSContext::IsShrinking is_shrinking = nsJSContext::NonShrinkingGC;
    337  if (!InIncrementalGC() && aStep.mReason == JS::GCReason::USER_INACTIVE) {
    338    bool do_gc = mWantAtLeastRegularGC;
    339 
    340    if (!mUserIsActive) {
    341      if (!nsRefreshDriver::IsRegularRateTimerTicking()) {
    342        mIsCompactingOnUserInactive = true;
    343        is_shrinking = nsJSContext::ShrinkingGC;
    344        do_gc = true;
    345      } else {
    346        // Poke again to restart the timer.
    347        PokeShrinkingGC();
    348      }
    349    }
    350 
    351    if (!do_gc) {
    352      using mozilla::ipc::IdleSchedulerChild;
    353      IdleSchedulerChild* child =
    354          IdleSchedulerChild::GetMainThreadIdleScheduler();
    355      if (child) {
    356        child->DoneGC();
    357      }
    358      NoteWontGC();
    359      KillGCRunner();
    360      return true;
    361    }
    362  }
    363 
    364  // Note that we are triggering the following GC slice and recording whether
    365  // it started in idle time, for use in the callback at the end of the slice.
    366  mTriggeredGCDeadline = Some(aDeadline);
    367 
    368  MOZ_ASSERT(mActiveIntersliceGCBudget);
    369  TimeStamp startTimeStamp = TimeStamp::Now();
    370  JS::SliceBudget budget = ComputeInterSliceGCBudget(aDeadline, startTimeStamp);
    371  nsJSContext::RunIncrementalGCSlice(aStep.mReason, is_shrinking, budget);
    372 
    373  // If the GC doesn't have any more work to do on the foreground thread (and
    374  // e.g. is waiting for background sweeping to finish) then return false to
    375  // make IdleTaskRunner postpone the next call a bit.
    376  JSContext* cx = dom::danger::GetJSContext();
    377  return JS::IncrementalGCHasForegroundWork(cx);
    378 }
    379 
    380 RefPtr<CCGCScheduler::MayGCPromise> CCGCScheduler::MayGCNow(
    381    JS::GCReason reason) {
    382  using namespace mozilla::ipc;
    383 
    384  // We ask the parent if we should GC for GCs that aren't too timely,
    385  // with the exception of MEM_PRESSURE, in that case we ask the parent
    386  // because GCing on too many processes at the same time when under
    387  // memory pressure could be a very bad experience for the user.
    388  switch (reason) {
    389    case JS::GCReason::PAGE_HIDE:
    390    case JS::GCReason::MEM_PRESSURE:
    391    case JS::GCReason::USER_INACTIVE:
    392    case JS::GCReason::FULL_GC_TIMER:
    393    case JS::GCReason::CC_FINISHED: {
    394      if (XRE_IsContentProcess()) {
    395        IdleSchedulerChild* child =
    396            IdleSchedulerChild::GetMainThreadIdleScheduler();
    397        if (child) {
    398          return child->MayGCNow();
    399        }
    400      }
    401      // The parent process doesn't ask IdleSchedulerParent if it can GC.
    402      break;
    403    }
    404    default:
    405      break;
    406  }
    407 
    408  // We use synchronous task dispatch here to avoid a trip through the event
    409  // loop if we're on the parent process or it's a GC reason that does not
    410  // require permission to GC.
    411  RefPtr<MayGCPromise::Private> p = MakeRefPtr<MayGCPromise::Private>(__func__);
    412  p->UseSynchronousTaskDispatch(__func__);
    413  p->Resolve(true, __func__);
    414  return p;
    415 }
    416 
    417 void CCGCScheduler::RunNextCollectorTimer(JS::GCReason aReason,
    418                                          mozilla::TimeStamp aDeadline) {
    419  if (mDidShutdown) {
    420    return;
    421  }
    422 
    423  // When we're in an incremental GC, we should always have an sGCRunner, so do
    424  // not check CC timers. The CC timers won't do anything during a GC.
    425  MOZ_ASSERT_IF(InIncrementalGC(), mGCRunner);
    426 
    427  RefPtr<IdleTaskRunner> runner;
    428  if (mGCRunner) {
    429    SetWantMajorGC(aReason);
    430    runner = mGCRunner;
    431  } else if (mCCRunner) {
    432    runner = mCCRunner;
    433  }
    434 
    435  if (runner) {
    436    runner->SetIdleDeadline(aDeadline);
    437    runner->Run();
    438  }
    439 }
    440 
    441 void CCGCScheduler::PokeShrinkingGC() {
    442  if (mShrinkingGCTimer || mDidShutdown) {
    443    return;
    444  }
    445 
    446  NS_NewTimerWithFuncCallback(
    447      &mShrinkingGCTimer,
    448      [](nsITimer* aTimer, void* aClosure) {
    449        CCGCScheduler* s = static_cast<CCGCScheduler*>(aClosure);
    450        s->KillShrinkingGCTimer();
    451        if (!s->mUserIsActive) {
    452          if (!nsRefreshDriver::IsRegularRateTimerTicking()) {
    453            s->SetWantMajorGC(JS::GCReason::USER_INACTIVE);
    454            if (!s->mHaveAskedParent) {
    455              s->EnsureGCRunner(0);
    456            }
    457          } else {
    458            s->PokeShrinkingGC();
    459          }
    460        }
    461      },
    462      this, StaticPrefs::javascript_options_compact_on_user_inactive_delay(),
    463      nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY, "ShrinkingGCTimerFired"_ns);
    464 }
    465 
    466 void CCGCScheduler::PokeFullGC() {
    467  if (!mFullGCTimer && !mDidShutdown) {
    468    NS_NewTimerWithFuncCallback(
    469        &mFullGCTimer,
    470        [](nsITimer* aTimer, void* aClosure) {
    471          CCGCScheduler* s = static_cast<CCGCScheduler*>(aClosure);
    472          s->KillFullGCTimer();
    473 
    474          // Even if the GC is denied by the parent process, because we've
    475          // set that we want a full GC we will get one eventually.
    476          s->SetNeedsFullGC();
    477          s->SetWantMajorGC(JS::GCReason::FULL_GC_TIMER);
    478          if (s->mCCRunner) {
    479            s->EnsureCCThenGC(CCReason::GC_WAITING);
    480          } else if (!s->mHaveAskedParent) {
    481            s->EnsureGCRunner(0);
    482          }
    483        },
    484        this, StaticPrefs::javascript_options_gc_delay_full(),
    485        nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY, "FullGCTimerFired"_ns);
    486  }
    487 }
    488 
    489 void CCGCScheduler::PokeGC(JS::GCReason aReason, JSObject* aObj,
    490                           TimeDuration aDelay) {
    491  MOZ_ASSERT(aReason != JS::GCReason::NO_REASON);
    492  MOZ_ASSERT(aReason != JS::GCReason::EAGER_NURSERY_COLLECTION);
    493 
    494  if (mDidShutdown) {
    495    return;
    496  }
    497 
    498  // If a post-CC GC was pending, then we'll make sure one is happening.
    499  mNeedsGCAfterCC = false;
    500 
    501  if (aObj) {
    502    JS::Zone* zone = JS::GetObjectZone(aObj);
    503    CycleCollectedJSRuntime::Get()->AddZoneWaitingForGC(zone);
    504  } else if (aReason != JS::GCReason::CC_FINISHED) {
    505    SetNeedsFullGC();
    506  }
    507 
    508  if (mGCRunner || mHaveAskedParent) {
    509    // There's already a GC runner, or there will be, so just return.
    510    return;
    511  }
    512 
    513  SetWantMajorGC(aReason);
    514 
    515  if (mCCRunner) {
    516    // Make sure CC is called regardless of the size of the purple buffer, and
    517    // GC after it.
    518    EnsureCCThenGC(CCReason::GC_WAITING);
    519    return;
    520  }
    521 
    522  // Wait for javascript.options.gc_delay (or delay_first) then start
    523  // looking for idle time to run the initial GC slice.
    524  static bool first = true;
    525  TimeDuration delay =
    526      aDelay ? aDelay
    527             : TimeDuration::FromMilliseconds(
    528                   first ? StaticPrefs::javascript_options_gc_delay_first()
    529                         : StaticPrefs::javascript_options_gc_delay());
    530  first = false;
    531  EnsureGCRunner(delay);
    532 }
    533 
    534 void CCGCScheduler::PokeMinorGC(JS::GCReason aReason) {
    535  MOZ_ASSERT(aReason != JS::GCReason::NO_REASON);
    536 
    537  if (mDidShutdown) {
    538    return;
    539  }
    540 
    541  SetWantEagerMinorGC(aReason);
    542 
    543  if (mGCRunner || mHaveAskedParent || mCCRunner) {
    544    // There's already a runner, or there will be, so just return.
    545    return;
    546  }
    547 
    548  // Immediately start looking for idle time to run the minor GC.
    549  EnsureGCRunner(0);
    550 }
    551 
    552 void CCGCScheduler::EnsureOrResetGCRunner() {
    553  if (!mGCRunner) {
    554    EnsureGCRunner(0);
    555    return;
    556  }
    557 
    558  mGCRunner->ResetTimer(TimeDuration::FromMilliseconds(
    559      StaticPrefs::javascript_options_gc_delay_interslice()));
    560 }
    561 
    562 TimeDuration CCGCScheduler::ComputeMinimumBudgetForRunner(
    563    TimeDuration aBaseValue) {
    564  // If the main thread was too busy to find idle for the whole last collection,
    565  // allow a very short budget this time.
    566  return mPreferFasterCollection ? TimeDuration::FromMilliseconds(1.0)
    567                                 : TimeDuration::FromMilliseconds(std::max(
    568                                       nsRefreshDriver::HighRateMultiplier() *
    569                                           aBaseValue.ToMilliseconds(),
    570                                       1.0));
    571 }
    572 
    573 void CCGCScheduler::EnsureGCRunner(TimeDuration aDelay) {
    574  if (mGCRunner) {
    575    return;
    576  }
    577 
    578  TimeDuration minimumBudget =
    579      ComputeMinimumBudgetForRunner(mActiveIntersliceGCBudget);
    580 
    581  // Wait at most the interslice GC delay before forcing a run.
    582  mGCRunner = IdleTaskRunner::Create(
    583      [this](TimeStamp aDeadline) { return GCRunnerFired(aDeadline); },
    584      "CCGCScheduler::EnsureGCRunner"_ns, aDelay,
    585      TimeDuration::FromMilliseconds(
    586          StaticPrefs::javascript_options_gc_delay_interslice()),
    587      minimumBudget, true, [this] { return mDidShutdown; },
    588      [this](uint32_t) {
    589        PROFILER_MARKER_UNTYPED("GC Interrupt", GCCC);
    590        mInterruptRequested = true;
    591      });
    592 }
    593 
    594 // nsJSEnvironmentObserver observes the user-interaction-inactive notifications
    595 // and triggers a shrinking a garbage collection if the user is still inactive
    596 // after NS_SHRINKING_GC_DELAY ms later, if the appropriate pref is set.
    597 void CCGCScheduler::UserIsInactive() {
    598  mUserIsActive = false;
    599  if (StaticPrefs::javascript_options_compact_on_user_inactive()) {
    600    PokeShrinkingGC();
    601  }
    602 }
    603 
    604 void CCGCScheduler::UserIsActive() {
    605  mUserIsActive = true;
    606  KillShrinkingGCTimer();
    607  if (mIsCompactingOnUserInactive) {
    608    mozilla::dom::AutoJSAPI jsapi;
    609    jsapi.Init();
    610    JS::AbortIncrementalGC(jsapi.cx());
    611  }
    612  MOZ_ASSERT(!mIsCompactingOnUserInactive);
    613 }
    614 
    615 void CCGCScheduler::KillShrinkingGCTimer() {
    616  if (mShrinkingGCTimer) {
    617    mShrinkingGCTimer->Cancel();
    618    NS_RELEASE(mShrinkingGCTimer);
    619  }
    620 }
    621 
    622 void CCGCScheduler::KillFullGCTimer() {
    623  if (mFullGCTimer) {
    624    mFullGCTimer->Cancel();
    625    NS_RELEASE(mFullGCTimer);
    626  }
    627 }
    628 
    629 void CCGCScheduler::KillGCRunner() {
    630  // If we're in an incremental GC then killing the timer is only okay if
    631  // we're shutting down.
    632  MOZ_ASSERT(!(InIncrementalGC() && !mDidShutdown));
    633  if (mGCRunner) {
    634    mGCRunner->Cancel();
    635    mGCRunner = nullptr;
    636  }
    637 }
    638 
    639 void CCGCScheduler::EnsureCCRunner(TimeDuration aDelay, TimeDuration aBudget) {
    640  MOZ_ASSERT(!mDidShutdown);
    641 
    642  TimeDuration minimumBudget = ComputeMinimumBudgetForRunner(aBudget);
    643 
    644  if (!mCCRunner) {
    645    mCCRunner = IdleTaskRunner::Create(
    646        [this](TimeStamp aDeadline) { return CCRunnerFired(aDeadline); },
    647        "EnsureCCRunner::CCRunnerFired"_ns, 0, aDelay, minimumBudget, true,
    648        [this] { return mDidShutdown; });
    649  } else {
    650    mCCRunner->SetMinimumUsefulBudget(minimumBudget.ToMilliseconds());
    651    nsIEventTarget* target = mozilla::GetCurrentSerialEventTarget();
    652    if (target) {
    653      mCCRunner->SetTimer(aDelay, target);
    654    }
    655  }
    656 }
    657 
    658 void CCGCScheduler::MaybePokeCC(TimeStamp aNow, uint32_t aSuspectedCCObjects) {
    659  if (mCCRunner || mDidShutdown) {
    660    return;
    661  }
    662 
    663  CCReason reason = ShouldScheduleCC(aNow, aSuspectedCCObjects);
    664  if (reason != CCReason::NO_REASON) {
    665    // We can kill some objects before running forgetSkippable.
    666    nsCycleCollector_dispatchDeferredDeletion();
    667 
    668    if (!mCCRunner) {
    669      InitCCRunnerStateMachine(CCRunnerState::ReducePurple, reason);
    670    }
    671    EnsureCCRunner(kCCSkippableDelay, kForgetSkippableSliceDuration);
    672  }
    673 }
    674 
    675 void CCGCScheduler::KillCCRunner() {
    676  UnblockCC();
    677  DeactivateCCRunner();
    678  if (mCCRunner) {
    679    mCCRunner->Cancel();
    680    mCCRunner = nullptr;
    681  }
    682 }
    683 
    684 void CCGCScheduler::KillAllTimersAndRunners() {
    685  KillShrinkingGCTimer();
    686  KillCCRunner();
    687  KillFullGCTimer();
    688  KillGCRunner();
    689 }
    690 
    691 JS::SliceBudget CCGCScheduler::ComputeCCSliceBudget(
    692    TimeStamp aDeadline, TimeStamp aCCBeginTime, TimeStamp aPrevSliceEndTime,
    693    TimeStamp aNow, bool* aPreferShorterSlices) const {
    694  *aPreferShorterSlices =
    695      aDeadline.IsNull() || (aDeadline - aNow) < kICCSliceBudget;
    696 
    697  TimeDuration baseBudget =
    698      aDeadline.IsNull() ? kICCSliceBudget : aDeadline - aNow;
    699 
    700  if (aPrevSliceEndTime.IsNull()) {
    701    // The first slice gets the standard slice time.
    702    return JS::SliceBudget(JS::TimeBudget(baseBudget));
    703  }
    704 
    705  // Only run a limited slice if we're within the max running time.
    706  MOZ_ASSERT(aNow >= aCCBeginTime);
    707  TimeDuration runningTime = aNow - aCCBeginTime;
    708  if (runningTime >= kMaxICCDuration) {
    709    return JS::SliceBudget::unlimited();
    710  }
    711 
    712  const TimeDuration maxSlice =
    713      TimeDuration::FromMilliseconds(MainThreadIdlePeriod::GetLongIdlePeriod());
    714 
    715  // Try to make up for a delay in running this slice.
    716  MOZ_ASSERT(aNow >= aPrevSliceEndTime);
    717  double sliceDelayMultiplier =
    718      (aNow - aPrevSliceEndTime) / kICCIntersliceDelay;
    719  TimeDuration delaySliceBudget =
    720      std::min(baseBudget.MultDouble(sliceDelayMultiplier), maxSlice);
    721 
    722  // Increase slice budgets up to |maxSlice| as we approach
    723  // half way through the ICC, to avoid large sync CCs.
    724  double percentToHalfDone =
    725      std::min(2.0 * (runningTime / kMaxICCDuration), 1.0);
    726  TimeDuration laterSliceBudget = maxSlice.MultDouble(percentToHalfDone);
    727 
    728  // Note: We may have already overshot the deadline, in which case
    729  // baseBudget will be negative and we will end up returning
    730  // laterSliceBudget.
    731  return JS::SliceBudget(JS::TimeBudget(
    732      std::max({delaySliceBudget, laterSliceBudget, baseBudget})));
    733 }
    734 
    735 JS::SliceBudget CCGCScheduler::ComputeInterSliceGCBudget(TimeStamp aDeadline,
    736                                                         TimeStamp aNow) {
    737  // We use longer budgets when the CC has been locked out but the CC has
    738  // tried to run since that means we may have a significant amount of
    739  // garbage to collect and it's better to GC in several longer slices than
    740  // in a very long one.
    741  TimeDuration budget =
    742      aDeadline.IsNull() ? mActiveIntersliceGCBudget * 2 : aDeadline - aNow;
    743  if (!mCCBlockStart) {
    744    return CreateGCSliceBudget(budget, !aDeadline.IsNull(), false);
    745  }
    746 
    747  TimeDuration blockedTime = aNow - mCCBlockStart;
    748  TimeDuration maxSliceGCBudget = mActiveIntersliceGCBudget * 10;
    749  double percentOfBlockedTime =
    750      std::min(blockedTime / kMaxCCLockedoutTime, 1.0);
    751  TimeDuration extendedBudget =
    752      maxSliceGCBudget.MultDouble(percentOfBlockedTime);
    753  if (budget >= extendedBudget) {
    754    return CreateGCSliceBudget(budget, !aDeadline.IsNull(), false);
    755  }
    756 
    757  // If the budget is being extended, do not allow it to be interrupted.
    758  auto result = JS::SliceBudget(JS::TimeBudget(extendedBudget), nullptr);
    759  result.idle = !aDeadline.IsNull();
    760  result.extended = true;
    761  return result;
    762 }
    763 
    764 CCReason CCGCScheduler::ShouldScheduleCC(TimeStamp aNow,
    765                                         uint32_t aSuspectedCCObjects) const {
    766  if (!mHasRunGC) {
    767    return CCReason::NO_REASON;
    768  }
    769 
    770  // Don't run consecutive CCs too often.
    771  if (mCleanupsSinceLastGC && !mLastCCEndTime.IsNull()) {
    772    if (aNow - mLastCCEndTime < kCCDelay) {
    773      return CCReason::NO_REASON;
    774    }
    775  }
    776 
    777  // If GC hasn't run recently and forget skippable only cycle was run,
    778  // don't start a new cycle too soon.
    779  if ((mCleanupsSinceLastGC > kMajorForgetSkippableCalls) &&
    780      !mLastForgetSkippableCycleEndTime.IsNull()) {
    781    if (aNow - mLastForgetSkippableCycleEndTime <
    782        kTimeBetweenForgetSkippableCycles) {
    783      return CCReason::NO_REASON;
    784    }
    785  }
    786 
    787  return IsCCNeeded(aNow, aSuspectedCCObjects);
    788 }
    789 
    790 CCRunnerStep CCGCScheduler::AdvanceCCRunner(TimeStamp aDeadline, TimeStamp aNow,
    791                                            uint32_t aSuspectedCCObjects) {
    792  struct StateDescriptor {
    793    // When in this state, should we first check to see if we still have
    794    // enough reason to CC?
    795    bool mCanAbortCC;
    796 
    797    // If we do decide to abort the CC, should we still try to forget
    798    // skippables one more time?
    799    bool mTryFinalForgetSkippable;
    800  };
    801 
    802  // The state descriptors for Inactive and Canceled will never actually be
    803  // used. We will never call this function while Inactive, and Canceled is
    804  // handled specially at the beginning.
    805  constexpr StateDescriptor stateDescriptors[] = {
    806      {false, false},  /* CCRunnerState::Inactive */
    807      {false, false},  /* CCRunnerState::ReducePurple */
    808      {true, true},    /* CCRunnerState::CleanupChildless */
    809      {true, false},   /* CCRunnerState::CleanupContentUnbinder */
    810      {false, false},  /* CCRunnerState::CleanupDeferred */
    811      {false, false},  /* CCRunnerState::StartCycleCollection */
    812      {false, false},  /* CCRunnerState::CycleCollecting */
    813      {false, false}}; /* CCRunnerState::Canceled */
    814  static_assert(std::size(stateDescriptors) == size_t(CCRunnerState::NumStates),
    815                "need one state descriptor per state");
    816  const StateDescriptor& desc = stateDescriptors[int(mCCRunnerState)];
    817 
    818  // Make sure we initialized the state machine.
    819  MOZ_ASSERT(mCCRunnerState != CCRunnerState::Inactive);
    820 
    821  if (mDidShutdown) {
    822    return {CCRunnerAction::StopRunning, Yield};
    823  }
    824 
    825  if (mCCRunnerState == CCRunnerState::Canceled) {
    826    // When we cancel a cycle, there may have been a final ForgetSkippable.
    827    return {CCRunnerAction::StopRunning, Yield};
    828  }
    829 
    830  if (InIncrementalGC()) {
    831    if (mCCBlockStart.IsNull()) {
    832      BlockCC(aNow);
    833 
    834      // If we have reached the CycleCollecting state, then ignore CC timer
    835      // fires while incremental GC is running. (Running ICC during an IGC
    836      // would cause us to synchronously finish the GC, which is bad.)
    837      //
    838      // If we have not yet started cycle collecting, then reset our state so
    839      // that we run forgetSkippable often enough before CC. Because of reduced
    840      // mCCDelay, forgetSkippable will be called just a few times.
    841      //
    842      // The kMaxCCLockedoutTime limit guarantees that we end up calling
    843      // forgetSkippable and CycleCollectNow eventually.
    844 
    845      if (mCCRunnerState != CCRunnerState::CycleCollecting) {
    846        mCCRunnerState = CCRunnerState::ReducePurple;
    847        mCCRunnerEarlyFireCount = 0;
    848        mCCDelay = kCCDelay / int64_t(3);
    849      }
    850      return {CCRunnerAction::None, Yield};
    851    }
    852 
    853    if (GetCCBlockedTime(aNow) < kMaxCCLockedoutTime) {
    854      return {CCRunnerAction::None, Yield};
    855    }
    856 
    857    // Locked out for too long, so proceed and finish the incremental GC
    858    // synchronously.
    859  }
    860 
    861  // For states that aren't just continuations of previous states, check
    862  // whether a CC is still needed (after doing various things to reduce the
    863  // purple buffer).
    864  if (desc.mCanAbortCC &&
    865      IsCCNeeded(aNow, aSuspectedCCObjects) == CCReason::NO_REASON) {
    866    // If we don't pass the threshold for wanting to cycle collect, stop now
    867    // (after possibly doing a final ForgetSkippable).
    868    mCCRunnerState = CCRunnerState::Canceled;
    869    NoteForgetSkippableOnlyCycle(aNow);
    870 
    871    // Preserve the previous code's idea of when to check whether a
    872    // ForgetSkippable should be fired.
    873    if (desc.mTryFinalForgetSkippable &&
    874        ShouldForgetSkippable(aSuspectedCCObjects)) {
    875      // The Canceled state will make us StopRunning after this action is
    876      // performed (see conditional at top of function).
    877      return {CCRunnerAction::ForgetSkippable, Yield, KeepChildless};
    878    }
    879 
    880    return {CCRunnerAction::StopRunning, Yield};
    881  }
    882 
    883  if (mEagerMinorGCReason != JS::GCReason::NO_REASON && !aDeadline.IsNull()) {
    884    return {CCRunnerAction::MinorGC, Continue, mEagerMinorGCReason};
    885  }
    886 
    887  switch (mCCRunnerState) {
    888      // ReducePurple: a GC ran (or we otherwise decided to try CC'ing). Wait
    889      // for some amount of time (kCCDelay, or less if incremental GC blocked
    890      // this CC) while firing regular ForgetSkippable actions before continuing
    891      // on.
    892    case CCRunnerState::ReducePurple:
    893      ++mCCRunnerEarlyFireCount;
    894      if (IsLastEarlyCCTimer(mCCRunnerEarlyFireCount)) {
    895        mCCRunnerState = CCRunnerState::CleanupChildless;
    896      }
    897 
    898      if (ShouldForgetSkippable(aSuspectedCCObjects)) {
    899        return {CCRunnerAction::ForgetSkippable, Yield, KeepChildless};
    900      }
    901 
    902      if (aDeadline.IsNull()) {
    903        return {CCRunnerAction::None, Yield};
    904      }
    905 
    906      // If we're called during idle time, try to find some work to do by
    907      // advancing to the next state, effectively bypassing some possible forget
    908      // skippable calls.
    909      mCCRunnerState = CCRunnerState::CleanupChildless;
    910 
    911      // Continue on to CleanupChildless, but only after checking IsCCNeeded
    912      // again.
    913      return {CCRunnerAction::None, Continue};
    914 
    915      // CleanupChildless: do a stronger ForgetSkippable that removes nodes with
    916      // no children in the cycle collector graph. This state is split into 3
    917      // parts; the other Cleanup* actions will happen within the same callback
    918      // (unless the ForgetSkippable shrinks the purple buffer enough for the CC
    919      // to be skipped entirely.)
    920    case CCRunnerState::CleanupChildless:
    921      mCCRunnerState = CCRunnerState::CleanupContentUnbinder;
    922      return {CCRunnerAction::ForgetSkippable, Yield, RemoveChildless};
    923 
    924      // CleanupContentUnbinder: continuing cleanup, clear out the content
    925      // unbinder.
    926    case CCRunnerState::CleanupContentUnbinder:
    927      if (aDeadline.IsNull()) {
    928        // Non-idle (waiting) callbacks skip the rest of the cleanup, but still
    929        // wait for another fire before the actual CC.
    930        mCCRunnerState = CCRunnerState::StartCycleCollection;
    931        return {CCRunnerAction::None, Yield};
    932      }
    933 
    934      // Running in an idle callback.
    935 
    936      // The deadline passed, so go straight to CC in the next slice.
    937      if (aNow >= aDeadline) {
    938        mCCRunnerState = CCRunnerState::StartCycleCollection;
    939        return {CCRunnerAction::None, Yield};
    940      }
    941 
    942      mCCRunnerState = CCRunnerState::CleanupDeferred;
    943      return {CCRunnerAction::CleanupContentUnbinder, Continue};
    944 
    945      // CleanupDeferred: continuing cleanup, do deferred deletion.
    946    case CCRunnerState::CleanupDeferred:
    947      MOZ_ASSERT(!aDeadline.IsNull(),
    948                 "Should only be in CleanupDeferred state when idle");
    949 
    950      // Our efforts to avoid a CC have failed. Let the timer fire once more
    951      // to trigger a CC.
    952      mCCRunnerState = CCRunnerState::StartCycleCollection;
    953      if (aNow >= aDeadline) {
    954        // The deadline passed, go straight to CC in the next slice.
    955        return {CCRunnerAction::None, Yield};
    956      }
    957 
    958      return {CCRunnerAction::CleanupDeferred, Yield};
    959 
    960      // StartCycleCollection: start actually doing cycle collection slices.
    961    case CCRunnerState::StartCycleCollection:
    962      // We are in the final timer fire and still meet the conditions for
    963      // triggering a CC. Let RunCycleCollectorSlice finish the current IGC if
    964      // any, because that will allow us to include the GC time in the CC pause.
    965      mCCRunnerState = CCRunnerState::CycleCollecting;
    966      [[fallthrough]];
    967 
    968      // CycleCollecting: continue running slices until done.
    969    case CCRunnerState::CycleCollecting: {
    970      CCRunnerStep step{CCRunnerAction::CycleCollect, Yield};
    971      step.mParam.mCCReason = mCCReason;
    972      mCCReason = CCReason::SLICE;  // Set reason for following slices.
    973      return step;
    974    }
    975 
    976    default:
    977      MOZ_CRASH("Unexpected CCRunner state");
    978  };
    979 }
    980 
    981 GCRunnerStep CCGCScheduler::GetNextGCRunnerAction(TimeStamp aDeadline) const {
    982  if (InIncrementalGC()) {
    983    MOZ_ASSERT(mMajorGCReason != JS::GCReason::NO_REASON);
    984    return {GCRunnerAction::GCSlice, mMajorGCReason};
    985  }
    986 
    987  // Service a non-eager GC request first, even if it requires waiting.
    988  if (mMajorGCReason != JS::GCReason::NO_REASON) {
    989    return {mReadyForMajorGC ? GCRunnerAction::StartMajorGC
    990                             : GCRunnerAction::WaitToMajorGC,
    991            mMajorGCReason};
    992  }
    993 
    994  // Now for eager requests, which are ignored unless we're idle.
    995  if (!aDeadline.IsNull()) {
    996    if (mEagerMajorGCReason != JS::GCReason::NO_REASON) {
    997      return {mReadyForMajorGC ? GCRunnerAction::StartMajorGC
    998                               : GCRunnerAction::WaitToMajorGC,
    999              mEagerMajorGCReason};
   1000    }
   1001 
   1002    if (mEagerMinorGCReason != JS::GCReason::NO_REASON) {
   1003      return {GCRunnerAction::MinorGC, mEagerMinorGCReason};
   1004    }
   1005  }
   1006 
   1007  return {GCRunnerAction::None, JS::GCReason::NO_REASON};
   1008 }
   1009 
   1010 JS::SliceBudget CCGCScheduler::ComputeForgetSkippableBudget(
   1011    TimeStamp aStartTimeStamp, TimeStamp aDeadline) {
   1012  if (mForgetSkippableFrequencyStartTime.IsNull()) {
   1013    mForgetSkippableFrequencyStartTime = aStartTimeStamp;
   1014  } else if (aStartTimeStamp - mForgetSkippableFrequencyStartTime >
   1015             kOneMinute) {
   1016    TimeStamp startPlusMinute = mForgetSkippableFrequencyStartTime + kOneMinute;
   1017 
   1018    // If we had forget skippables only at the beginning of the interval, we
   1019    // still want to use the whole time, minute or more, for frequency
   1020    // calculation. mLastForgetSkippableEndTime is needed if forget skippable
   1021    // takes enough time to push the interval to be over a minute.
   1022    TimeStamp endPoint = std::max(startPlusMinute, mLastForgetSkippableEndTime);
   1023 
   1024    // Duration in minutes.
   1025    double duration =
   1026        (endPoint - mForgetSkippableFrequencyStartTime).ToSeconds() / 60;
   1027    uint32_t frequencyPerMinute = uint32_t(mForgetSkippableCounter / duration);
   1028    glean::dom::forget_skippable_frequency.AccumulateSingleSample(
   1029        frequencyPerMinute);
   1030    mForgetSkippableCounter = 0;
   1031    mForgetSkippableFrequencyStartTime = aStartTimeStamp;
   1032  }
   1033  ++mForgetSkippableCounter;
   1034 
   1035  TimeDuration budgetTime =
   1036      aDeadline ? (aDeadline - aStartTimeStamp) : kForgetSkippableSliceDuration;
   1037  return JS::SliceBudget(budgetTime);
   1038 }
   1039 
   1040 }  // namespace mozilla