/* 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; // Returns null if we shouldn't GC now (eg a GC is already running). static RefPtr 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 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 mTriggeredGCDeadline; RefPtr mGCRunner; RefPtr 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