forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1076 lines
		
	
	
	
		
			38 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			1076 lines
		
	
	
	
		
			38 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /* 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::GetCurrentEventTarget();
 | |
|     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
 | 
