summaryrefslogtreecommitdiffstats
path: root/dom/base/CCGCScheduler.h
diff options
context:
space:
mode:
Diffstat (limited to 'dom/base/CCGCScheduler.h')
-rw-r--r--dom/base/CCGCScheduler.h541
1 files changed, 541 insertions, 0 deletions
diff --git a/dom/base/CCGCScheduler.h b/dom/base/CCGCScheduler.h
new file mode 100644
index 0000000000..3d35af8da4
--- /dev/null
+++ b/dom/base/CCGCScheduler.h
@@ -0,0 +1,541 @@
+/* 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"
+#include "nsCycleCollectionParticipant.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)
+// and doing the actual CC.
+static const TimeDuration kCCDelay = TimeDuration::FromSeconds(6);
+
+static const TimeDuration kCCSkippableDelay =
+ TimeDuration::FromMilliseconds(250);
+
+// In case the cycle collector isn't run at all, we don't want forget skippables
+// to run too often. So limit the forget skippable cycle to start at earliest 2
+// seconds after the end of the previous cycle.
+static const TimeDuration kTimeBetweenForgetSkippableCycles =
+ TimeDuration::FromSeconds(2);
+
+// ForgetSkippable is usually fast, so we can use small budgets.
+// This isn't a real budget but a hint to IdleTaskRunner whether there
+// is enough time to call ForgetSkippable.
+static const TimeDuration kForgetSkippableSliceDuration =
+ TimeDuration::FromMilliseconds(2);
+
+// Maximum amount of time that should elapse between incremental CC slices
+static const TimeDuration kICCIntersliceDelay =
+ TimeDuration::FromMilliseconds(64);
+
+// Time budget for an incremental CC slice when using timer to run it.
+static const TimeDuration kICCSliceBudget = TimeDuration::FromMilliseconds(3);
+// Minimum budget for an incremental CC slice when using idle time to run it.
+static const TimeDuration kIdleICCSliceBudget =
+ TimeDuration::FromMilliseconds(2);
+
+// Maximum total duration for an ICC
+static const TimeDuration kMaxICCDuration = TimeDuration::FromSeconds(2);
+
+// Force a CC after this long if there's more than NS_CC_FORCED_PURPLE_LIMIT
+// objects in the purple buffer.
+static const TimeDuration kCCForced = kOneMinute * 2;
+static const uint32_t kCCForcedPurpleLimit = 10;
+
+// Don't allow an incremental GC to lock out the CC for too long.
+static const TimeDuration kMaxCCLockedoutTime = TimeDuration::FromSeconds(30);
+
+// Trigger a CC if the purple buffer exceeds this size when we check it.
+static const uint32_t kCCPurpleLimit = 200;
+
+// Actions performed by the GCRunner state machine.
+enum class GCRunnerAction {
+ MinorGC, // Run a minor GC (nursery collection)
+ WaitToMajorGC, // We want to start a new major GC
+ StartMajorGC, // The parent says we may begin our major GC
+ GCSlice, // Run a single slice of a major GC
+ None
+};
+
+struct GCRunnerStep {
+ GCRunnerAction mAction;
+ JS::GCReason mReason;
+};
+
+// Actions that are output from the CCRunner state machine.
+enum class CCRunnerAction {
+ // Do nothing.
+ None,
+
+ // We crossed an eager minor GC threshold in the middle of an incremental CC,
+ // and we have some idle time.
+ MinorGC,
+
+ // Various cleanup actions.
+ ForgetSkippable,
+ CleanupContentUnbinder,
+ CleanupDeferred,
+
+ // Do the actual cycle collection (build the graph etc).
+ CycleCollect,
+
+ // All done.
+ StopRunning
+};
+
+enum CCRunnerYield { Continue, Yield };
+
+enum CCRunnerForgetSkippableRemoveChildless {
+ KeepChildless = false,
+ RemoveChildless = true
+};
+
+struct CCRunnerStep {
+ // The action the scheduler is instructing the caller to perform.
+ CCRunnerAction mAction;
+
+ // Whether to stop processing actions for this invocation of the timer
+ // callback.
+ CCRunnerYield mYield;
+
+ union ActionData {
+ // If the action is ForgetSkippable, then whether to remove childless nodes
+ // or not.
+ CCRunnerForgetSkippableRemoveChildless mRemoveChildless;
+
+ // If the action is CycleCollect, the reason for the collection.
+ CCReason mCCReason;
+
+ // If the action is MinorGC, the reason for the GC.
+ JS::GCReason mReason;
+
+ MOZ_IMPLICIT ActionData(CCRunnerForgetSkippableRemoveChildless v)
+ : mRemoveChildless(v) {}
+ MOZ_IMPLICIT ActionData(CCReason v) : mCCReason(v) {}
+ MOZ_IMPLICIT ActionData(JS::GCReason v) : mReason(v) {}
+ ActionData() = default;
+ } mParam;
+};
+
+class CCGCScheduler {
+ public:
+ CCGCScheduler()
+ : mAskParentBeforeMajorGC(XRE_IsContentProcess()),
+ mReadyForMajorGC(!mAskParentBeforeMajorGC),
+ mInterruptRequested(false) {}
+
+ static bool CCRunnerFired(TimeStamp aDeadline);
+
+ // Parameter setting
+
+ void SetActiveIntersliceGCBudget(TimeDuration aDuration) {
+ mActiveIntersliceGCBudget = aDuration;
+ }
+
+ // State retrieval
+
+ TimeDuration GetCCBlockedTime(TimeStamp aNow) const {
+ MOZ_ASSERT(mInIncrementalGC);
+ MOZ_ASSERT(!mCCBlockStart.IsNull());
+ return aNow - mCCBlockStart;
+ }
+
+ bool InIncrementalGC() const { return mInIncrementalGC; }
+
+ 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, TimeDuration aDelay = 0);
+ void PokeShrinkingGC();
+ void PokeFullGC();
+ void MaybePokeCC(TimeStamp aNow, uint32_t aSuspectedCCObjects);
+ void PokeMinorGC(JS::GCReason aReason);
+
+ void UserIsInactive();
+ void UserIsActive();
+ bool IsUserActive() const { return mUserIsActive; }
+
+ void KillShrinkingGCTimer();
+ void KillFullGCTimer();
+ void KillGCRunner();
+ void KillCCRunner();
+ void KillAllTimersAndRunners();
+
+ js::SliceBudget CreateGCSliceBudget(mozilla::TimeDuration aDuration,
+ bool isIdle, bool isExtended) {
+ auto budget = js::SliceBudget(aDuration, &mInterruptRequested);
+ budget.idle = isIdle;
+ budget.extended = isExtended;
+ return budget;
+ }
+
+ /*
+ * 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(TimeDuration aDelay);
+
+ void EnsureCCRunner(TimeDuration aDelay, TimeDuration aBudget);
+
+ // State modification
+
+ void SetNeedsFullGC(bool aNeedGC = true) { mNeedsFullGC = aNeedGC; }
+
+ void SetWantMajorGC(JS::GCReason aReason) {
+ MOZ_ASSERT(aReason != JS::GCReason::NO_REASON);
+
+ // If the GC being requested is not a shrinking GC set this flag.
+ // If/when the shrinking GC timer fires but the user is active we check
+ // this flag before canceling the GC, so as not to cancel the
+ // non-shrinking GC being requested here.
+ if (aReason != JS::GCReason::USER_INACTIVE) {
+ mWantAtLeastRegularGC = true;
+ }
+
+ // Force full GCs when called from reftests so that we collect dead zones
+ // that have not been scheduled for collection.
+ if (aReason == JS::GCReason::DOM_WINDOW_UTILS) {
+ SetNeedsFullGC();
+ }
+
+ // USER_INACTIVE trumps everything,
+ // FULL_GC_TIMER trumps everything except USER_INACTIVE,
+ // all other reasons just use the latest reason.
+ switch (aReason) {
+ case JS::GCReason::USER_INACTIVE:
+ mMajorGCReason = aReason;
+ break;
+ case JS::GCReason::FULL_GC_TIMER:
+ if (mMajorGCReason != JS::GCReason::USER_INACTIVE) {
+ mMajorGCReason = aReason;
+ }
+ break;
+ default:
+ if (mMajorGCReason != JS::GCReason::USER_INACTIVE &&
+ mMajorGCReason != JS::GCReason::FULL_GC_TIMER) {
+ mMajorGCReason = aReason;
+ }
+ break;
+ }
+ }
+
+ void SetWantEagerMinorGC(JS::GCReason aReason) {
+ if (mEagerMinorGCReason == JS::GCReason::NO_REASON) {
+ mEagerMinorGCReason = aReason;
+ }
+ }
+
+ // Ensure that the current runner does a cycle collection, and trigger a GC
+ // after it finishes.
+ void EnsureCCThenGC(CCReason aReason) {
+ MOZ_ASSERT(mCCRunnerState != CCRunnerState::Inactive);
+ MOZ_ASSERT(aReason != CCReason::NO_REASON);
+ mNeedsFullCC = aReason;
+ mNeedsGCAfterCC = true;
+ }
+
+ // Returns false if we started and finished a major GC while waiting for a
+ // response.
+ [[nodiscard]] bool NoteReadyForMajorGC() {
+ if (mMajorGCReason == JS::GCReason::NO_REASON || InIncrementalGC()) {
+ return false;
+ }
+ mReadyForMajorGC = true;
+ return true;
+ }
+
+ // Starting a major GC (incremental or non-incremental).
+ void NoteGCBegin(JS::GCReason aReason);
+
+ // Major GC completed.
+ void NoteGCEnd();
+
+ // A timer fired, but then decided not to run a GC.
+ void NoteWontGC();
+
+ void NoteMinorGCEnd() { mEagerMinorGCReason = JS::GCReason::NO_REASON; }
+
+ // This is invoked when we reach the actual cycle collection portion of the
+ // overall cycle collection.
+ void NoteCCBegin(CCReason aReason, TimeStamp aWhen,
+ uint32_t aNumForgetSkippables, uint32_t aSuspected,
+ uint32_t aRemovedPurples);
+
+ // This is invoked when the whole process of collection is done -- i.e., CC
+ // preparation (eg ForgetSkippables) in addition to the CC itself. There
+ // really ought to be a separate name for the overall CC as opposed to the
+ // actual cycle collection portion.
+ void NoteCCEnd(const CycleCollectorResults& aResults, TimeStamp aWhen,
+ mozilla::TimeDuration aMaxSliceTime);
+
+ // A single slice has completed.
+ void NoteGCSliceEnd(TimeStamp aStart, TimeStamp aEnd);
+
+ bool GCRunnerFired(TimeStamp aDeadline);
+ bool GCRunnerFiredDoGC(TimeStamp aDeadline, const GCRunnerStep& aStep);
+
+ using MayGCPromise =
+ MozPromise<bool, 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;
+ }
+
+ void UnblockCC() { mCCBlockStart = TimeStamp(); }
+
+ // Returns the number of purple buffer items that were processed and removed.
+ uint32_t NoteForgetSkippableComplete(TimeStamp aNow,
+ uint32_t aSuspectedBeforeForgetSkippable,
+ uint32_t aSuspectedCCObjects) {
+ mLastForgetSkippableEndTime = aNow;
+ mPreviousSuspectedCount = aSuspectedCCObjects;
+ mCleanupsSinceLastGC++;
+ return aSuspectedBeforeForgetSkippable - aSuspectedCCObjects;
+ }
+
+ // Test if we are in the NoteCCBegin .. NoteCCEnd interval.
+ bool IsCollectingCycles() const { return mIsCollectingCycles; }
+
+ // The CC was abandoned without running a slice, so we only did forget
+ // skippables. Prevent running another cycle soon.
+ void NoteForgetSkippableOnlyCycle(TimeStamp aNow) {
+ mLastForgetSkippableCycleEndTime = aNow;
+ }
+
+ void Shutdown() {
+ mDidShutdown = true;
+ KillAllTimersAndRunners();
+ }
+
+ // 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,
+ TimeStamp aNow,
+ bool* aPreferShorterSlices) const;
+
+ js::SliceBudget ComputeInterSliceGCBudget(TimeStamp aDeadline,
+ TimeStamp aNow);
+
+ bool ShouldForgetSkippable(uint32_t aSuspectedCCObjects) 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) <= aSuspectedCCObjects) ||
+ mCleanupsSinceLastGC < kMajorForgetSkippableCalls;
+ }
+
+ // There is reason to suspect that there may be a significant amount of
+ // garbage to cycle collect: either we just finished a GC, or the purple
+ // buffer is getting really big, or it's getting somewhat big and it has been
+ // too long since the last CC.
+ CCReason IsCCNeeded(TimeStamp aNow, uint32_t aSuspectedCCObjects) const {
+ if (mNeedsFullCC != CCReason::NO_REASON) {
+ return mNeedsFullCC;
+ }
+ if (aSuspectedCCObjects > kCCPurpleLimit) {
+ return CCReason::MANY_SUSPECTED;
+ }
+ if (aSuspectedCCObjects > kCCForcedPurpleLimit && mLastCCEndTime &&
+ aNow - mLastCCEndTime > kCCForced) {
+ return CCReason::TIMED;
+ }
+ return CCReason::NO_REASON;
+ }
+
+ mozilla::CCReason ShouldScheduleCC(TimeStamp aNow,
+ uint32_t aSuspectedCCObjects) 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;
+ }
+
+ bool IsLastEarlyCCTimer(int32_t aCurrentFireCount) const {
+ int32_t numEarlyTimerFires =
+ std::max(int32_t(mCCDelay / kCCSkippableDelay) - 2, 1);
+
+ return aCurrentFireCount >= numEarlyTimerFires;
+ }
+
+ enum class CCRunnerState {
+ Inactive,
+ ReducePurple,
+ CleanupChildless,
+ CleanupContentUnbinder,
+ CleanupDeferred,
+ StartCycleCollection,
+ CycleCollecting,
+ Canceled,
+ NumStates
+ };
+
+ void InitCCRunnerStateMachine(CCRunnerState initialState, CCReason aReason) {
+ if (mCCRunner) {
+ return;
+ }
+
+ MOZ_ASSERT(mCCReason == CCReason::NO_REASON);
+ mCCReason = aReason;
+
+ // 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.
+ if (initialState == CCRunnerState::ReducePurple) {
+ mCCDelay = kCCDelay;
+ mCCRunnerEarlyFireCount = 0;
+ } else if (initialState == CCRunnerState::CycleCollecting) {
+ // Nothing needed.
+ } else {
+ MOZ_CRASH("Invalid initial state");
+ }
+ }
+
+ void DeactivateCCRunner() {
+ mCCRunnerState = CCRunnerState::Inactive;
+ mCCReason = CCReason::NO_REASON;
+ }
+
+ bool HasMoreIdleGCRunnerWork() const {
+ return mMajorGCReason != JS::GCReason::NO_REASON ||
+ mEagerMajorGCReason != JS::GCReason::NO_REASON ||
+ mEagerMinorGCReason != JS::GCReason::NO_REASON;
+ }
+
+ GCRunnerStep GetNextGCRunnerAction(TimeStamp aDeadline) const;
+
+ CCRunnerStep AdvanceCCRunner(TimeStamp aDeadline, TimeStamp aNow,
+ uint32_t aSuspectedCCObjects);
+
+ // 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
+
+ // An incremental GC is in progress, which blocks the CC from running for its
+ // duration (or until it goes too long and is finished synchronously.)
+ bool mInIncrementalGC = false;
+
+ // Whether to ask the parent process if now is a good time to GC (false for
+ // the parent process.)
+ const bool mAskParentBeforeMajorGC;
+
+ // We've asked the parent process if now is a good time to GC (do not ask
+ // again).
+ bool mHaveAskedParent = false;
+
+ // The parent process is ready for us to do a major GC.
+ bool mReadyForMajorGC;
+
+ // Set when the IdleTaskRunner requests the current task be interrupted.
+ // Cleared when the GC slice budget has detected the interrupt request.
+ js::SliceBudget::InterruptRequestFlag mInterruptRequested;
+
+ // When a shrinking GC has been requested but we back-out, if this is true
+ // we run a non-shrinking GC.
+ bool mWantAtLeastRegularGC = false;
+
+ // When the CC started actually waiting for the GC to finish. This will be
+ // set to non-null at a later time than mCCLockedOut.
+ TimeStamp mCCBlockStart;
+
+ bool mDidShutdown = false;
+
+ TimeStamp mLastForgetSkippableEndTime;
+ uint32_t mForgetSkippableCounter = 0;
+ TimeStamp mForgetSkippableFrequencyStartTime;
+ TimeStamp mLastCCEndTime;
+ TimeStamp mLastForgetSkippableCycleEndTime;
+
+ CCRunnerState mCCRunnerState = CCRunnerState::Inactive;
+ int32_t mCCRunnerEarlyFireCount = 0;
+ TimeDuration mCCDelay = kCCDelay;
+
+ // Prevent the very first CC from running before we have GC'd and set the
+ // gray bits.
+ bool mHasRunGC = false;
+
+ mozilla::CCReason mNeedsFullCC = CCReason::NO_REASON;
+ bool mNeedsFullGC = true;
+ bool mNeedsGCAfterCC = false;
+ uint32_t mPreviousSuspectedCount = 0;
+
+ uint32_t mCleanupsSinceLastGC = UINT32_MAX;
+
+ // If the GC runner triggers a GC slice, this will be set to the idle deadline
+ // or the null timestamp if non-idle. It will be Nothing at the end of an
+ // internally-triggered slice.
+ mozilla::Maybe<TimeStamp> mTriggeredGCDeadline;
+
+ RefPtr<IdleTaskRunner> mGCRunner;
+ RefPtr<IdleTaskRunner> mCCRunner;
+ nsITimer* mShrinkingGCTimer = nullptr;
+ nsITimer* mFullGCTimer = nullptr;
+
+ mozilla::CCReason mCCReason = mozilla::CCReason::NO_REASON;
+ JS::GCReason mMajorGCReason = JS::GCReason::NO_REASON;
+ JS::GCReason mEagerMajorGCReason = JS::GCReason::NO_REASON;
+ JS::GCReason mEagerMinorGCReason = JS::GCReason::NO_REASON;
+
+ bool mIsCompactingOnUserInactive = false;
+ bool mIsCollectingCycles = false;
+ bool mUserIsActive = true;
+
+ public:
+ uint32_t mCCollectedWaitingForGC = 0;
+ uint32_t mCCollectedZonesWaitingForGC = 0;
+ uint32_t mLikelyShortLivingObjectsNeedingGC = 0;
+
+ // Configuration parameters
+
+ TimeDuration mActiveIntersliceGCBudget = TimeDuration::FromMilliseconds(5);
+};
+
+} // namespace mozilla