diff options
Diffstat (limited to 'dom/base/CCGCScheduler.h')
-rw-r--r-- | dom/base/CCGCScheduler.h | 541 |
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..e7bc4a42e7 --- /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. + mozilla::Atomic<bool> 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 |