summaryrefslogtreecommitdiffstats
path: root/dom/base/CCGCScheduler.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/base/CCGCScheduler.cpp1076
1 files changed, 1076 insertions, 0 deletions
diff --git a/dom/base/CCGCScheduler.cpp b/dom/base/CCGCScheduler.cpp
new file mode 100644
index 0000000000..260290b3db
--- /dev/null
+++ b/dom/base/CCGCScheduler.cpp
@@ -0,0 +1,1076 @@
+/* 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 "js/GCAPI.h"
+#include "mozilla/StaticPrefs_javascript.h"
+#include "mozilla/CycleCollectedJSRuntime.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/PerfStats.h"
+#include "nsRefreshDriver.h"
+
+/*
+ * GC Scheduling from Firefox
+ * ==========================
+ *
+ * See also GC Scheduling from SpiderMonkey's perspective here:
+ * https://searchfox.org/mozilla-central/source/js/src/gc/Scheduling.h
+ *
+ * From Firefox's perspective GCs can start in 5 different ways:
+ *
+ * * The JS engine just starts doing a GC for its own reasons (see above).
+ * Firefox finds out about these via a callback in nsJSEnvironment.cpp
+ * * PokeGC()
+ * * PokeFullGC()
+ * * PokeShrinkingGC()
+ * * memory-pressure GCs (via a listener in nsJSEnvironment.cpp).
+ *
+ * PokeGC
+ * ------
+ *
+ * void CCGCScheduler::PokeGC(JS::GCReason aReason, JSObject* aObj,
+ * TimeDuration aDelay)
+ *
+ * PokeGC provides a way for callers to say "Hey, there may be some memory
+ * associated with this object (via Zone) you can collect." PokeGC will:
+ * * add the zone to a set,
+ * * set flags including what kind of GC to run (SetWantMajorGC),
+ * * then creates the mGCRunner with a short delay.
+ *
+ * The delay can allow other calls to PokeGC to add their zones so they can
+ * be collected together.
+ *
+ * See below for what happens when mGCRunner fires.
+ *
+ * PokeFullGC
+ * ----------
+ *
+ * void CCGCScheduler::PokeFullGC()
+ *
+ * PokeFullGC will create a timer that will initiate a "full" (all zones)
+ * collection. This is usually used after a regular collection if a full GC
+ * seems like a good idea (to collect inter-zone references).
+ *
+ * When the timer fires it will:
+ * * set flags (SetWantMajorGC),
+ * * start the mGCRunner with zero delay.
+ *
+ * See below for when mGCRunner fires.
+ *
+ * PokeShrinkingGC
+ * ---------------
+ *
+ * void CCGCScheduler::PokeShrinkingGC()
+ *
+ * PokeShrinkingGC is called when Firefox's user is inactive.
+ * Like PokeFullGC, PokeShrinkingGC uses a timer, but the timeout is longer
+ * which should prevent the ShrinkingGC from starting if the user only
+ * glances away for a brief time. When the timer fires it will:
+ *
+ * * set flags (SetWantMajorGC),
+ * * create the mGCRunner.
+ *
+ * There is a check if the user is still inactive in GCRunnerFired), if the
+ * user has become active the shrinking GC is canceled and either a regular
+ * GC (if requested, see mWantAtLeastRegularGC) or no GC is run.
+ *
+ * When mGCRunner fires
+ * --------------------
+ *
+ * When mGCRunner fires it calls GCRunnerFired. This starts in the
+ * WaitToMajorGC state:
+ *
+ * * If this is a parent process it jumps to the next state
+ * * If this is a content process it will ask the parent if now is a good
+ * time to do a GC. (MayGCNow)
+ * * kill the mGCRunner
+ * * Exit
+ *
+ * Meanwhile the parent process will queue GC requests so that not too many
+ * are running in parallel overwhelming the CPU cores (see
+ * IdleSchedulerParent).
+ *
+ * When the promise from MayGCNow is resolved it will set some
+ * state (NoteReadyForMajorGC) and restore the mGCRunner.
+ *
+ * When the mGCRunner runs a second time (or this is the parent process and
+ * which jumped over the above logic. It will be in the StartMajorGC state.
+ * It will initiate the GC for real, usually. If it's a shrinking GC and the
+ * user is now active again it may abort. See GCRunnerFiredDoGC().
+ *
+ * The runner will then run the first slice of the garbage collection.
+ * Later slices are also run by the runner, the final slice kills the runner
+ * from the GC callback in nsJSEnvironment.cpp.
+ *
+ * There is additional logic in the code to handle concurrent requests of
+ * various kinds.
+ */
+
+namespace geckoprofiler::markers {
+struct CCIntervalMarker {
+ static constexpr mozilla::Span<const char> MarkerTypeName() {
+ return mozilla::MakeStringSpan("CC");
+ }
+ static void StreamJSONMarkerData(
+ mozilla::baseprofiler::SpliceableJSONWriter& aWriter, bool aIsStart,
+ const mozilla::ProfilerString8View& aReason,
+ uint32_t aForgetSkippableBeforeCC, uint32_t aSuspectedAtCCStart,
+ uint32_t aRemovedPurples, const mozilla::CycleCollectorResults& aResults,
+ mozilla::TimeDuration aMaxSliceTime) {
+ if (aIsStart) {
+ aWriter.StringProperty("mReason", aReason);
+ aWriter.IntProperty("mSuspected", aSuspectedAtCCStart);
+ aWriter.IntProperty("mForgetSkippable", aForgetSkippableBeforeCC);
+ aWriter.IntProperty("mRemovedPurples", aRemovedPurples);
+ } else {
+ aWriter.TimeDoubleMsProperty("mMaxSliceTime",
+ aMaxSliceTime.ToMilliseconds());
+ aWriter.IntProperty("mSlices", aResults.mNumSlices);
+
+ aWriter.BoolProperty("mAnyManual", aResults.mAnyManual);
+ aWriter.BoolProperty("mForcedGC", aResults.mForcedGC);
+ aWriter.BoolProperty("mMergedZones", aResults.mMergedZones);
+ aWriter.IntProperty("mVisitedRefCounted", aResults.mVisitedRefCounted);
+ aWriter.IntProperty("mVisitedGCed", aResults.mVisitedGCed);
+ aWriter.IntProperty("mFreedRefCounted", aResults.mFreedRefCounted);
+ aWriter.IntProperty("mFreedGCed", aResults.mFreedGCed);
+ aWriter.IntProperty("mFreedJSZones", aResults.mFreedJSZones);
+ }
+ }
+ static mozilla::MarkerSchema MarkerTypeDisplay() {
+ using MS = mozilla::MarkerSchema;
+ MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable,
+ MS::Location::TimelineMemory};
+ schema.AddStaticLabelValue(
+ "Description",
+ "Summary data for the core part of a cycle collection, possibly "
+ "encompassing a set of incremental slices. The main thread is not "
+ "blocked for the entire major CC interval, only for the individual "
+ "slices.");
+ schema.AddKeyLabelFormatSearchable("mReason", "Reason", MS::Format::String,
+ MS::Searchable::Searchable);
+ schema.AddKeyLabelFormat("mMaxSliceTime", "Max Slice Time",
+ MS::Format::Duration);
+ schema.AddKeyLabelFormat("mSuspected", "Suspected Objects",
+ MS::Format::Integer);
+ schema.AddKeyLabelFormat("mSlices", "Number of Slices",
+ MS::Format::Integer);
+ schema.AddKeyLabelFormat("mAnyManual", "Manually Triggered",
+ MS::Format::Integer);
+ schema.AddKeyLabelFormat("mForcedGC", "GC Forced", MS::Format::Integer);
+ schema.AddKeyLabelFormat("mMergedZones", "Zones Merged",
+ MS::Format::Integer);
+ schema.AddKeyLabelFormat("mForgetSkippable", "Forget Skippables",
+ MS::Format::Integer);
+ schema.AddKeyLabelFormat("mVisitedRefCounted", "Refcounted Objects Visited",
+ MS::Format::Integer);
+ schema.AddKeyLabelFormat("mVisitedGCed", "GC Objects Visited",
+ MS::Format::Integer);
+ schema.AddKeyLabelFormat("mFreedRefCounted", "Refcounted Objects Freed",
+ MS::Format::Integer);
+ schema.AddKeyLabelFormat("mFreedGCed", "GC Objects Freed",
+ MS::Format::Integer);
+ schema.AddKeyLabelFormat("mCollectedGCZones", "JS Zones Freed",
+ MS::Format::Integer);
+ schema.AddKeyLabelFormat("mRemovedPurples",
+ "Objects Removed From Purple Buffer",
+ MS::Format::Integer);
+ return schema;
+ }
+};
+} // namespace geckoprofiler::markers
+
+namespace mozilla {
+
+void CCGCScheduler::NoteGCBegin(JS::GCReason aReason) {
+ // Treat all GC as incremental here; non-incremental GC will just appear to
+ // be one slice.
+ mInIncrementalGC = true;
+ mReadyForMajorGC = !mAskParentBeforeMajorGC;
+
+ // Tell the parent process that we've started a GC (it might not know if
+ // we hit a threshold in the JS engine).
+ using mozilla::ipc::IdleSchedulerChild;
+ IdleSchedulerChild* child = IdleSchedulerChild::GetMainThreadIdleScheduler();
+ if (child) {
+ child->StartedGC();
+ }
+
+ // The reason might have come from mMajorReason, mEagerMajorGCReason, or
+ // in the case of an internally-generated GC, it might come from the
+ // internal logic (and be passed in here). It's easier to manage a single
+ // reason state variable, so merge all sources into mMajorGCReason.
+ MOZ_ASSERT(aReason != JS::GCReason::NO_REASON);
+ mMajorGCReason = aReason;
+ mEagerMajorGCReason = JS::GCReason::NO_REASON;
+}
+
+void CCGCScheduler::NoteGCEnd() {
+ mMajorGCReason = JS::GCReason::NO_REASON;
+ mEagerMajorGCReason = JS::GCReason::NO_REASON;
+ mEagerMinorGCReason = JS::GCReason::NO_REASON;
+
+ mInIncrementalGC = false;
+ mCCBlockStart = TimeStamp();
+ mReadyForMajorGC = !mAskParentBeforeMajorGC;
+ mWantAtLeastRegularGC = false;
+ mNeedsFullCC = CCReason::GC_FINISHED;
+ mHasRunGC = true;
+ mIsCompactingOnUserInactive = false;
+
+ mCleanupsSinceLastGC = 0;
+ mCCollectedWaitingForGC = 0;
+ mCCollectedZonesWaitingForGC = 0;
+ mLikelyShortLivingObjectsNeedingGC = 0;
+
+ using mozilla::ipc::IdleSchedulerChild;
+ IdleSchedulerChild* child = IdleSchedulerChild::GetMainThreadIdleScheduler();
+ if (child) {
+ child->DoneGC();
+ }
+}
+
+void CCGCScheduler::NoteGCSliceEnd(TimeStamp aStart, TimeStamp aEnd) {
+ 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;
+
+ MOZ_ASSERT(aEnd >= aStart);
+ TimeDuration sliceDuration = aEnd - aStart;
+ PerfStats::RecordMeasurement(PerfStats::Metric::MajorGC, sliceDuration);
+
+ // Compute how much GC time was spent in predicted-to-be-idle time. In the
+ // unlikely event that the slice started after the deadline had already
+ // passed, treat the entire slice as non-idle.
+ TimeDuration nonIdleDuration;
+ bool startedIdle = mTriggeredGCDeadline.isSome() &&
+ !mTriggeredGCDeadline->IsNull() &&
+ *mTriggeredGCDeadline > aStart;
+ if (!startedIdle) {
+ nonIdleDuration = sliceDuration;
+ } else {
+ if (*mTriggeredGCDeadline < aEnd) {
+ // Overran the idle deadline.
+ nonIdleDuration = aEnd - *mTriggeredGCDeadline;
+ }
+ }
+
+ PerfStats::RecordMeasurement(PerfStats::Metric::NonIdleMajorGC,
+ nonIdleDuration);
+
+ // Note the GC_SLICE_DURING_IDLE previously had a different definition: it was
+ // a histogram of percentages of externally-triggered slices. It is now a
+ // histogram of percentages of all slices. That means that now you might have
+ // a 4ms internal slice (0% during idle) followed by a 16ms external slice
+ // (15ms during idle), whereas before this would show up as a single record of
+ // a single slice with 75% of its time during idle (15 of 20ms).
+ TimeDuration idleDuration = sliceDuration - nonIdleDuration;
+ uint32_t percent =
+ uint32_t(idleDuration.ToSeconds() / sliceDuration.ToSeconds() * 100);
+ Telemetry::Accumulate(Telemetry::GC_SLICE_DURING_IDLE, percent);
+
+ mTriggeredGCDeadline.reset();
+}
+
+void CCGCScheduler::NoteCCBegin(CCReason aReason, TimeStamp aWhen,
+ uint32_t aNumForgetSkippables,
+ uint32_t aSuspected, uint32_t aRemovedPurples) {
+ CycleCollectorResults ignoredResults;
+ PROFILER_MARKER(
+ "CC", GCCC, MarkerOptions(MarkerTiming::IntervalStart(aWhen)),
+ CCIntervalMarker,
+ /* aIsStart */ true,
+ ProfilerString8View::WrapNullTerminatedString(CCReasonToString(aReason)),
+ aNumForgetSkippables, aSuspected, aRemovedPurples, ignoredResults,
+ TimeDuration());
+
+ mIsCollectingCycles = true;
+}
+
+void CCGCScheduler::NoteCCEnd(const CycleCollectorResults& aResults,
+ TimeStamp aWhen,
+ mozilla::TimeDuration aMaxSliceTime) {
+ mCCollectedWaitingForGC += aResults.mFreedGCed;
+ mCCollectedZonesWaitingForGC += aResults.mFreedJSZones;
+
+ PROFILER_MARKER("CC", GCCC, MarkerOptions(MarkerTiming::IntervalEnd(aWhen)),
+ CCIntervalMarker, /* aIsStart */ false, nullptr, 0, 0, 0,
+ aResults, aMaxSliceTime);
+
+ mIsCollectingCycles = false;
+ mLastCCEndTime = aWhen;
+ mNeedsFullCC = CCReason::NO_REASON;
+}
+
+void CCGCScheduler::NoteWontGC() {
+ mReadyForMajorGC = !mAskParentBeforeMajorGC;
+ mMajorGCReason = JS::GCReason::NO_REASON;
+ mEagerMajorGCReason = JS::GCReason::NO_REASON;
+ mWantAtLeastRegularGC = false;
+ // Don't clear the WantFullGC state, we will do a full GC the next time a
+ // GC happens for any other reason.
+}
+
+bool CCGCScheduler::GCRunnerFired(TimeStamp aDeadline) {
+ MOZ_ASSERT(!mDidShutdown, "GCRunner still alive during shutdown");
+
+ GCRunnerStep step = GetNextGCRunnerAction(aDeadline);
+ switch (step.mAction) {
+ case GCRunnerAction::None:
+ KillGCRunner();
+ return false;
+
+ case GCRunnerAction::MinorGC:
+ JS::MaybeRunNurseryCollection(CycleCollectedJSRuntime::Get()->Runtime(),
+ step.mReason);
+ NoteMinorGCEnd();
+ return HasMoreIdleGCRunnerWork();
+
+ case GCRunnerAction::WaitToMajorGC: {
+ MOZ_ASSERT(!mHaveAskedParent, "GCRunner alive after asking the parent");
+ RefPtr<CCGCScheduler::MayGCPromise> mbPromise =
+ CCGCScheduler::MayGCNow(step.mReason);
+ if (!mbPromise) {
+ // We can GC now.
+ break;
+ }
+
+ mHaveAskedParent = true;
+ KillGCRunner();
+ mbPromise->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [this](bool aMayGC) {
+ mHaveAskedParent = false;
+ if (aMayGC) {
+ if (!NoteReadyForMajorGC()) {
+ // Another GC started and maybe completed while waiting.
+ return;
+ }
+ // Recreate the GC runner with a 0 delay. The new runner will
+ // continue in idle time.
+ KillGCRunner();
+ EnsureGCRunner(0);
+ } else if (!InIncrementalGC()) {
+ // We should kill the GC runner since we're done with it, but
+ // only if there's no incremental GC.
+ KillGCRunner();
+ NoteWontGC();
+ }
+ },
+ [this](mozilla::ipc::ResponseRejectReason r) {
+ mHaveAskedParent = false;
+ if (!InIncrementalGC()) {
+ KillGCRunner();
+ NoteWontGC();
+ }
+ });
+
+ return true;
+ }
+
+ case GCRunnerAction::StartMajorGC:
+ case GCRunnerAction::GCSlice:
+ break;
+ }
+
+ return GCRunnerFiredDoGC(aDeadline, step);
+}
+
+bool CCGCScheduler::GCRunnerFiredDoGC(TimeStamp aDeadline,
+ const GCRunnerStep& aStep) {
+ // Run a GC slice, possibly the first one of a major GC.
+ nsJSContext::IsShrinking is_shrinking = nsJSContext::NonShrinkingGC;
+ if (!InIncrementalGC() && aStep.mReason == JS::GCReason::USER_INACTIVE) {
+ bool do_gc = mWantAtLeastRegularGC;
+
+ if (!mUserIsActive) {
+ if (!nsRefreshDriver::IsRegularRateTimerTicking()) {
+ mIsCompactingOnUserInactive = true;
+ is_shrinking = nsJSContext::ShrinkingGC;
+ do_gc = true;
+ } else {
+ // Poke again to restart the timer.
+ PokeShrinkingGC();
+ }
+ }
+
+ if (!do_gc) {
+ using mozilla::ipc::IdleSchedulerChild;
+ IdleSchedulerChild* child =
+ IdleSchedulerChild::GetMainThreadIdleScheduler();
+ if (child) {
+ child->DoneGC();
+ }
+ NoteWontGC();
+ KillGCRunner();
+ return true;
+ }
+ }
+
+ // Note that we are triggering the following GC slice and recording whether
+ // it started in idle time, for use in the callback at the end of the slice.
+ mTriggeredGCDeadline = Some(aDeadline);
+
+ MOZ_ASSERT(mActiveIntersliceGCBudget);
+ TimeStamp startTimeStamp = TimeStamp::Now();
+ js::SliceBudget budget = ComputeInterSliceGCBudget(aDeadline, startTimeStamp);
+ nsJSContext::RunIncrementalGCSlice(aStep.mReason, is_shrinking, budget);
+
+ // 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;
+ }
+
+ // We use synchronous task dispatch here to avoid a trip through the event
+ // loop if we're on the parent process or it's a GC reason that does not
+ // require permission to GC.
+ RefPtr<MayGCPromise::Private> p = MakeRefPtr<MayGCPromise::Private>(__func__);
+ p->UseSynchronousTaskDispatch(__func__);
+ p->Resolve(true, __func__);
+ return p;
+}
+
+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) {
+ CCGCScheduler* s = static_cast<CCGCScheduler*>(aClosure);
+ s->KillShrinkingGCTimer();
+ if (!s->mUserIsActive) {
+ if (!nsRefreshDriver::IsRegularRateTimerTicking()) {
+ s->SetWantMajorGC(JS::GCReason::USER_INACTIVE);
+ if (!s->mHaveAskedParent) {
+ s->EnsureGCRunner(0);
+ }
+ } else {
+ s->PokeShrinkingGC();
+ }
+ }
+ },
+ 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) {
+ CCGCScheduler* s = static_cast<CCGCScheduler*>(aClosure);
+ s->KillFullGCTimer();
+
+ // Even if the GC is denied by the parent process, because we've
+ // set that we want a full GC we will get one eventually.
+ s->SetNeedsFullGC();
+ s->SetWantMajorGC(JS::GCReason::FULL_GC_TIMER);
+ if (!s->mHaveAskedParent) {
+ s->EnsureGCRunner(0);
+ }
+ },
+ this, StaticPrefs::javascript_options_gc_delay_full(),
+ nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY, "FullGCTimerFired");
+ }
+}
+
+void CCGCScheduler::PokeGC(JS::GCReason aReason, JSObject* aObj,
+ TimeDuration aDelay) {
+ MOZ_ASSERT(aReason != JS::GCReason::NO_REASON);
+ MOZ_ASSERT(aReason != JS::GCReason::EAGER_NURSERY_COLLECTION);
+
+ if (mDidShutdown) {
+ return;
+ }
+
+ // If a post-CC GC was pending, then we'll make sure one is happening.
+ mNeedsGCAfterCC = false;
+
+ if (aObj) {
+ JS::Zone* zone = JS::GetObjectZone(aObj);
+ CycleCollectedJSRuntime::Get()->AddZoneWaitingForGC(zone);
+ } else if (aReason != JS::GCReason::CC_FINISHED) {
+ SetNeedsFullGC();
+ }
+
+ if (mGCRunner || mHaveAskedParent) {
+ // There's already a GC runner, or there will be, so 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(CCReason::GC_WAITING);
+ 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;
+ TimeDuration delay =
+ aDelay ? aDelay
+ : TimeDuration::FromMilliseconds(
+ first ? StaticPrefs::javascript_options_gc_delay_first()
+ : StaticPrefs::javascript_options_gc_delay());
+ first = false;
+ EnsureGCRunner(delay);
+}
+
+void CCGCScheduler::PokeMinorGC(JS::GCReason aReason) {
+ MOZ_ASSERT(aReason != JS::GCReason::NO_REASON);
+
+ if (mDidShutdown) {
+ return;
+ }
+
+ SetWantEagerMinorGC(aReason);
+
+ if (mGCRunner || mHaveAskedParent || mCCRunner) {
+ // There's already a runner, or there will be, so just return.
+ return;
+ }
+
+ // Immediately start looking for idle time to run the minor GC.
+ EnsureGCRunner(0);
+}
+
+void CCGCScheduler::EnsureGCRunner(TimeDuration aDelay) {
+ if (mGCRunner) {
+ return;
+ }
+
+ TimeDuration minimumBudget = nsRefreshDriver::IsInHighRateMode()
+ ? TimeDuration::FromMilliseconds(1)
+ : mActiveIntersliceGCBudget;
+
+ // Wait at most the interslice GC delay before forcing a run.
+ mGCRunner = IdleTaskRunner::Create(
+ [this](TimeStamp aDeadline) { return GCRunnerFired(aDeadline); },
+ "CCGCScheduler::EnsureGCRunner", aDelay,
+ TimeDuration::FromMilliseconds(
+ StaticPrefs::javascript_options_gc_delay_interslice()),
+ minimumBudget, true, [this] { return mDidShutdown; },
+ [this](uint32_t) {
+ PROFILER_MARKER_UNTYPED("GC Interrupt", GCCC);
+ mInterruptRequested = true;
+ });
+}
+
+// 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::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 we're in an incremental GC then killing the timer is only okay if
+ // we're shutting down.
+ MOZ_ASSERT(!(InIncrementalGC() && !mDidShutdown));
+ if (mGCRunner) {
+ mGCRunner->Cancel();
+ mGCRunner = nullptr;
+ }
+}
+
+void CCGCScheduler::EnsureCCRunner(TimeDuration aDelay, TimeDuration aBudget) {
+ MOZ_ASSERT(!mDidShutdown);
+
+ TimeDuration minimumBudget = nsRefreshDriver::IsInHighRateMode()
+ ? TimeDuration::FromMilliseconds(1)
+ : aBudget;
+
+ if (!mCCRunner) {
+ mCCRunner = IdleTaskRunner::Create(
+ CCRunnerFired, "EnsureCCRunner::CCRunnerFired", 0, aDelay,
+ minimumBudget, true, [this] { return mDidShutdown; });
+ } else {
+ mCCRunner->SetMinimumUsefulBudget(minimumBudget.ToMilliseconds());
+ nsIEventTarget* target = mozilla::GetCurrentSerialEventTarget();
+ if (target) {
+ mCCRunner->SetTimer(aDelay, target);
+ }
+ }
+}
+
+void CCGCScheduler::MaybePokeCC(TimeStamp aNow, uint32_t aSuspectedCCObjects) {
+ if (mCCRunner || mDidShutdown) {
+ return;
+ }
+
+ CCReason reason = ShouldScheduleCC(aNow, aSuspectedCCObjects);
+ if (reason != CCReason::NO_REASON) {
+ // We can kill some objects before running forgetSkippable.
+ nsCycleCollector_dispatchDeferredDeletion();
+
+ if (!mCCRunner) {
+ InitCCRunnerStateMachine(CCRunnerState::ReducePurple, reason);
+ }
+ 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,
+ TimeStamp aNow, bool* aPreferShorterSlices) const {
+ *aPreferShorterSlices =
+ aDeadline.IsNull() || (aDeadline - aNow) < kICCSliceBudget;
+
+ TimeDuration baseBudget =
+ aDeadline.IsNull() ? kICCSliceBudget : aDeadline - aNow;
+
+ if (aPrevSliceEndTime.IsNull()) {
+ // The first slice gets the standard slice time.
+ return js::SliceBudget(js::TimeBudget(baseBudget));
+ }
+
+ // Only run a limited slice if we're within the max running time.
+ MOZ_ASSERT(aNow >= aCCBeginTime);
+ TimeDuration runningTime = aNow - 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(aNow >= aPrevSliceEndTime);
+ double sliceDelayMultiplier =
+ (aNow - 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})));
+}
+
+js::SliceBudget CCGCScheduler::ComputeInterSliceGCBudget(TimeStamp aDeadline,
+ TimeStamp aNow) {
+ // 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 CreateGCSliceBudget(budget, !aDeadline.IsNull(), false);
+ }
+
+ TimeDuration blockedTime = aNow - mCCBlockStart;
+ TimeDuration maxSliceGCBudget = mActiveIntersliceGCBudget * 10;
+ double percentOfBlockedTime =
+ std::min(blockedTime / kMaxCCLockedoutTime, 1.0);
+ TimeDuration extendedBudget =
+ maxSliceGCBudget.MultDouble(percentOfBlockedTime);
+ if (budget >= extendedBudget) {
+ return CreateGCSliceBudget(budget, !aDeadline.IsNull(), false);
+ }
+
+ // If the budget is being extended, do not allow it to be interrupted.
+ auto result = js::SliceBudget(js::TimeBudget(extendedBudget), nullptr);
+ result.idle = !aDeadline.IsNull();
+ result.extended = true;
+ return result;
+}
+
+CCReason CCGCScheduler::ShouldScheduleCC(TimeStamp aNow,
+ uint32_t aSuspectedCCObjects) const {
+ if (!mHasRunGC) {
+ return CCReason::NO_REASON;
+ }
+
+ // Don't run consecutive CCs too often.
+ if (mCleanupsSinceLastGC && !mLastCCEndTime.IsNull()) {
+ if (aNow - mLastCCEndTime < kCCDelay) {
+ return CCReason::NO_REASON;
+ }
+ }
+
+ // 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 (aNow - mLastForgetSkippableCycleEndTime <
+ kTimeBetweenForgetSkippableCycles) {
+ return CCReason::NO_REASON;
+ }
+ }
+
+ return IsCCNeeded(aNow, aSuspectedCCObjects);
+}
+
+CCRunnerStep CCGCScheduler::AdvanceCCRunner(TimeStamp aDeadline, TimeStamp aNow,
+ uint32_t aSuspectedCCObjects) {
+ 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};
+ }
+
+ if (InIncrementalGC()) {
+ if (mCCBlockStart.IsNull()) {
+ BlockCC(aNow);
+
+ // 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(aNow) < 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(aNow, aSuspectedCCObjects) == CCReason::NO_REASON) {
+ // If we don't pass the threshold for wanting to cycle collect, stop now
+ // (after possibly doing a final ForgetSkippable).
+ mCCRunnerState = CCRunnerState::Canceled;
+ NoteForgetSkippableOnlyCycle(aNow);
+
+ // Preserve the previous code's idea of when to check whether a
+ // ForgetSkippable should be fired.
+ if (desc.mTryFinalForgetSkippable &&
+ ShouldForgetSkippable(aSuspectedCCObjects)) {
+ // 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};
+ }
+
+ if (mEagerMinorGCReason != JS::GCReason::NO_REASON && !aDeadline.IsNull()) {
+ return {CCRunnerAction::MinorGC, Continue, mEagerMinorGCReason};
+ }
+
+ 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(aSuspectedCCObjects)) {
+ 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 (aNow >= 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 (aNow >= 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: {
+ CCRunnerStep step{CCRunnerAction::CycleCollect, Yield};
+ step.mParam.mCCReason = mCCReason;
+ mCCReason = CCReason::SLICE; // Set reason for following slices.
+ return step;
+ }
+
+ default:
+ MOZ_CRASH("Unexpected CCRunner state");
+ };
+}
+
+GCRunnerStep CCGCScheduler::GetNextGCRunnerAction(TimeStamp aDeadline) const {
+ if (InIncrementalGC()) {
+ MOZ_ASSERT(mMajorGCReason != JS::GCReason::NO_REASON);
+ return {GCRunnerAction::GCSlice, mMajorGCReason};
+ }
+
+ // Service a non-eager GC request first, even if it requires waiting.
+ if (mMajorGCReason != JS::GCReason::NO_REASON) {
+ return {mReadyForMajorGC ? GCRunnerAction::StartMajorGC
+ : GCRunnerAction::WaitToMajorGC,
+ mMajorGCReason};
+ }
+
+ // Now for eager requests, which are ignored unless we're idle.
+ if (!aDeadline.IsNull()) {
+ if (mEagerMajorGCReason != JS::GCReason::NO_REASON) {
+ return {mReadyForMajorGC ? GCRunnerAction::StartMajorGC
+ : GCRunnerAction::WaitToMajorGC,
+ mEagerMajorGCReason};
+ }
+
+ if (mEagerMinorGCReason != JS::GCReason::NO_REASON) {
+ return {GCRunnerAction::MinorGC, mEagerMinorGCReason};
+ }
+ }
+
+ return {GCRunnerAction::None, JS::GCReason::NO_REASON};
+}
+
+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