Source code

Revision control

Copy as Markdown

Other Tools

/* 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 {
extern const TimeDuration kOneMinute;
// The amount of time we wait between a request to CC (after GC ran)
// and doing the actual CC.
extern const TimeDuration kCCDelay;
extern const TimeDuration kCCSkippableDelay;
// 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.
extern const TimeDuration kTimeBetweenForgetSkippableCycles;
// 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.
extern const TimeDuration kForgetSkippableSliceDuration;
// Maximum amount of time that should elapse between incremental CC slices
extern const TimeDuration kICCIntersliceDelay;
// Time budget for an incremental CC slice when using timer to run it.
extern const TimeDuration kICCSliceBudget;
// Minimum budget for an incremental CC slice when using idle time to run it.
extern const TimeDuration kIdleICCSliceBudget;
// Maximum total duration for an ICC
extern const TimeDuration kMaxICCDuration;
// Force a CC after this long if there's more than NS_CC_FORCED_PURPLE_LIMIT
// objects in the purple buffer.
extern const TimeDuration kCCForced;
constexpr uint32_t kCCForcedPurpleLimit = 10;
// Don't allow an incremental GC to lock out the CC for too long.
extern const TimeDuration kMaxCCLockedoutTime;
// Trigger a CC if the purple buffer exceeds this size when we check it.
constexpr 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) {
mInterruptRequested = false;
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);
// If GCRunner isn't active, this calls EnsureGCRunner(0). Otherwise the timer
// is reset.
void EnsureOrResetGCRunner();
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