1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 #include "CCGCScheduler.h"
8 #include "mozilla/StaticPrefs_javascript.h"
9 #include "mozilla/CycleCollectedJSRuntime.h"
10 #include "mozilla/ProfilerMarkers.h"
11 #include "mozilla/dom/ScriptSettings.h"
12 #include "mozilla/PerfStats.h"
13 #include "nsRefreshDriver.h"
15 /* Globally initialized constants
18 const TimeDuration kOneMinute
= TimeDuration::FromSeconds(60.0f
);
19 const TimeDuration kCCDelay
= TimeDuration::FromSeconds(6);
20 const TimeDuration kCCSkippableDelay
= TimeDuration::FromMilliseconds(250);
21 const TimeDuration kTimeBetweenForgetSkippableCycles
=
22 TimeDuration::FromSeconds(2);
23 const TimeDuration kForgetSkippableSliceDuration
=
24 TimeDuration::FromMilliseconds(2);
25 const TimeDuration kICCIntersliceDelay
= TimeDuration::FromMilliseconds(250);
26 const TimeDuration kICCSliceBudget
= TimeDuration::FromMilliseconds(3);
27 const TimeDuration kIdleICCSliceBudget
= TimeDuration::FromMilliseconds(2);
28 const TimeDuration kMaxICCDuration
= TimeDuration::FromSeconds(2);
30 const TimeDuration kCCForced
= kOneMinute
* 2;
31 const TimeDuration kMaxCCLockedoutTime
= TimeDuration::FromSeconds(30);
32 } // namespace mozilla
35 * GC Scheduling from Firefox
36 * ==========================
38 * See also GC Scheduling from SpiderMonkey's perspective here:
39 * https://searchfox.org/mozilla-central/source/js/src/gc/Scheduling.h
41 * From Firefox's perspective GCs can start in 5 different ways:
43 * * The JS engine just starts doing a GC for its own reasons (see above).
44 * Firefox finds out about these via a callback in nsJSEnvironment.cpp
48 * * memory-pressure GCs (via a listener in nsJSEnvironment.cpp).
53 * void CCGCScheduler::PokeGC(JS::GCReason aReason, JSObject* aObj,
54 * TimeDuration aDelay)
56 * PokeGC provides a way for callers to say "Hey, there may be some memory
57 * associated with this object (via Zone) you can collect." PokeGC will:
58 * * add the zone to a set,
59 * * set flags including what kind of GC to run (SetWantMajorGC),
60 * * then creates the mGCRunner with a short delay.
62 * The delay can allow other calls to PokeGC to add their zones so they can
63 * be collected together.
65 * See below for what happens when mGCRunner fires.
70 * void CCGCScheduler::PokeFullGC()
72 * PokeFullGC will create a timer that will initiate a "full" (all zones)
73 * collection. This is usually used after a regular collection if a full GC
74 * seems like a good idea (to collect inter-zone references).
76 * When the timer fires it will:
77 * * set flags (SetWantMajorGC),
78 * * start the mGCRunner with zero delay.
80 * See below for when mGCRunner fires.
85 * void CCGCScheduler::PokeShrinkingGC()
87 * PokeShrinkingGC is called when Firefox's user is inactive.
88 * Like PokeFullGC, PokeShrinkingGC uses a timer, but the timeout is longer
89 * which should prevent the ShrinkingGC from starting if the user only
90 * glances away for a brief time. When the timer fires it will:
92 * * set flags (SetWantMajorGC),
93 * * create the mGCRunner.
95 * There is a check if the user is still inactive in GCRunnerFired), if the
96 * user has become active the shrinking GC is canceled and either a regular
97 * GC (if requested, see mWantAtLeastRegularGC) or no GC is run.
99 * When mGCRunner fires
100 * --------------------
102 * When mGCRunner fires it calls GCRunnerFired. This starts in the
103 * WaitToMajorGC state:
105 * * If this is a parent process it jumps to the next state
106 * * If this is a content process it will ask the parent if now is a good
107 * time to do a GC. (MayGCNow)
108 * * kill the mGCRunner
111 * Meanwhile the parent process will queue GC requests so that not too many
112 * are running in parallel overwhelming the CPU cores (see
113 * IdleSchedulerParent).
115 * When the promise from MayGCNow is resolved it will set some
116 * state (NoteReadyForMajorGC) and restore the mGCRunner.
118 * When the mGCRunner runs a second time (or this is the parent process and
119 * which jumped over the above logic. It will be in the StartMajorGC state.
120 * It will initiate the GC for real, usually. If it's a shrinking GC and the
121 * user is now active again it may abort. See GCRunnerFiredDoGC().
123 * The runner will then run the first slice of the garbage collection.
124 * Later slices are also run by the runner, the final slice kills the runner
125 * from the GC callback in nsJSEnvironment.cpp.
127 * There is additional logic in the code to handle concurrent requests of
131 namespace geckoprofiler::markers
{
132 struct CCIntervalMarker
: public mozilla::BaseMarkerType
<CCIntervalMarker
> {
133 static constexpr const char* Name
= "CC";
134 static constexpr const char* Description
=
135 "Summary data for the core part of a cycle collection, possibly "
136 "encompassing a set of incremental slices. The main thread is not "
137 "blocked for the entire major CC interval, only for the individual "
140 using MS
= mozilla::MarkerSchema
;
141 static constexpr MS::PayloadField PayloadFields
[] = {
142 {"mReason", MS::InputType::CString
, "Reason", MS::Format::String
,
143 MS::PayloadFlags::Searchable
},
144 {"mMaxSliceTime", MS::InputType::TimeDuration
, "Max Slice Time",
145 MS::Format::Duration
},
146 {"mSuspected", MS::InputType::Uint32
, "Suspected Objects",
147 MS::Format::Integer
},
148 {"mSlices", MS::InputType::Uint32
, "Number of Slices",
149 MS::Format::Integer
},
150 {"mAnyManual", MS::InputType::Boolean
, "Manually Triggered",
151 MS::Format::Integer
},
152 {"mForcedGC", MS::InputType::Boolean
, "GC Forced", MS::Format::Integer
},
153 {"mMergedZones", MS::InputType::Boolean
, "Zones Merged",
154 MS::Format::Integer
},
155 {"mForgetSkippable", MS::InputType::Uint32
, "Forget Skippables",
156 MS::Format::Integer
},
157 {"mVisitedRefCounted", MS::InputType::Uint32
,
158 "Refcounted Objects Visited", MS::Format::Integer
},
159 {"mVisitedGCed", MS::InputType::Uint32
, "GC Objects Visited",
160 MS::Format::Integer
},
161 {"mFreedRefCounted", MS::InputType::Uint32
, "GC Objects Freed",
162 MS::Format::Integer
},
163 {"mFreedGCed", MS::InputType::Uint32
, "GC Objects Freed",
164 MS::Format::Integer
},
165 {"mFreedJSZones", MS::InputType::Uint32
, "JS Zones Freed",
166 MS::Format::Integer
},
167 {"mRemovedPurples", MS::InputType::Uint32
,
168 "Objects Removed From Purple Buffer", MS::Format::Integer
}};
170 static constexpr MS::Location Locations
[] = {MS::Location::MarkerChart
,
171 MS::Location::MarkerTable
,
172 MS::Location::TimelineMemory
};
173 static constexpr MS::ETWMarkerGroup Group
= MS::ETWMarkerGroup::Memory
;
175 static void TranslateMarkerInputToSchema(
176 void* aContext
, bool aIsStart
,
177 const mozilla::ProfilerString8View
& aReason
,
178 uint32_t aForgetSkippableBeforeCC
, uint32_t aSuspectedAtCCStart
,
179 uint32_t aRemovedPurples
, const mozilla::CycleCollectorResults
& aResults
,
180 const mozilla::TimeDuration
& aMaxSliceTime
) {
183 ETW::OutputMarkerSchema(aContext
, CCIntervalMarker
{}, aReason
,
184 mozilla::TimeDuration
{}, aSuspectedAtCCStart
,
185 none
, false, false, false,
186 aForgetSkippableBeforeCC
, none
, none
, none
, none
,
187 none
, aRemovedPurples
);
189 ETW::OutputMarkerSchema(
190 aContext
, CCIntervalMarker
{}, mozilla::ProfilerStringView(""),
191 aMaxSliceTime
, none
, aResults
.mNumSlices
, aResults
.mAnyManual
,
192 aResults
.mForcedGC
, aResults
.mMergedZones
, none
,
193 aResults
.mVisitedRefCounted
, aResults
.mVisitedGCed
,
194 aResults
.mFreedRefCounted
, aResults
.mFreedGCed
,
195 aResults
.mFreedJSZones
, none
);
199 static void StreamJSONMarkerData(
200 mozilla::baseprofiler::SpliceableJSONWriter
& aWriter
, bool aIsStart
,
201 const mozilla::ProfilerString8View
& aReason
,
202 uint32_t aForgetSkippableBeforeCC
, uint32_t aSuspectedAtCCStart
,
203 uint32_t aRemovedPurples
, const mozilla::CycleCollectorResults
& aResults
,
204 mozilla::TimeDuration aMaxSliceTime
) {
206 aWriter
.StringProperty("mReason", aReason
);
207 aWriter
.IntProperty("mSuspected", aSuspectedAtCCStart
);
208 aWriter
.IntProperty("mForgetSkippable", aForgetSkippableBeforeCC
);
209 aWriter
.IntProperty("mRemovedPurples", aRemovedPurples
);
211 aWriter
.TimeDoubleMsProperty("mMaxSliceTime",
212 aMaxSliceTime
.ToMilliseconds());
213 aWriter
.IntProperty("mSlices", aResults
.mNumSlices
);
215 aWriter
.BoolProperty("mAnyManual", aResults
.mAnyManual
);
216 aWriter
.BoolProperty("mForcedGC", aResults
.mForcedGC
);
217 aWriter
.BoolProperty("mMergedZones", aResults
.mMergedZones
);
218 aWriter
.IntProperty("mVisitedRefCounted", aResults
.mVisitedRefCounted
);
219 aWriter
.IntProperty("mVisitedGCed", aResults
.mVisitedGCed
);
220 aWriter
.IntProperty("mFreedRefCounted", aResults
.mFreedRefCounted
);
221 aWriter
.IntProperty("mFreedGCed", aResults
.mFreedGCed
);
222 aWriter
.IntProperty("mFreedJSZones", aResults
.mFreedJSZones
);
226 } // namespace geckoprofiler::markers
230 void CCGCScheduler::NoteGCBegin(JS::GCReason aReason
) {
231 // Treat all GC as incremental here; non-incremental GC will just appear to
233 mInIncrementalGC
= true;
234 mReadyForMajorGC
= !mAskParentBeforeMajorGC
;
236 // Tell the parent process that we've started a GC (it might not know if
237 // we hit a threshold in the JS engine).
238 using mozilla::ipc::IdleSchedulerChild
;
239 IdleSchedulerChild
* child
= IdleSchedulerChild::GetMainThreadIdleScheduler();
244 // The reason might have come from mMajorReason, mEagerMajorGCReason, or
245 // in the case of an internally-generated GC, it might come from the
246 // internal logic (and be passed in here). It's easier to manage a single
247 // reason state variable, so merge all sources into mMajorGCReason.
248 MOZ_ASSERT(aReason
!= JS::GCReason::NO_REASON
);
249 mMajorGCReason
= aReason
;
250 mEagerMajorGCReason
= JS::GCReason::NO_REASON
;
253 void CCGCScheduler::NoteGCEnd() {
254 mMajorGCReason
= JS::GCReason::NO_REASON
;
255 mEagerMajorGCReason
= JS::GCReason::NO_REASON
;
256 mEagerMinorGCReason
= JS::GCReason::NO_REASON
;
258 mInIncrementalGC
= false;
259 mCCBlockStart
= TimeStamp();
260 mReadyForMajorGC
= !mAskParentBeforeMajorGC
;
261 mWantAtLeastRegularGC
= false;
262 mNeedsFullCC
= CCReason::GC_FINISHED
;
264 mIsCompactingOnUserInactive
= false;
266 mCleanupsSinceLastGC
= 0;
267 mCCollectedWaitingForGC
= 0;
268 mCCollectedZonesWaitingForGC
= 0;
269 mLikelyShortLivingObjectsNeedingGC
= 0;
271 using mozilla::ipc::IdleSchedulerChild
;
272 IdleSchedulerChild
* child
= IdleSchedulerChild::GetMainThreadIdleScheduler();
278 void CCGCScheduler::NoteGCSliceEnd(TimeStamp aStart
, TimeStamp aEnd
) {
279 if (mMajorGCReason
== JS::GCReason::NO_REASON
) {
280 // Internally-triggered GCs do not wait for the parent's permission to
281 // proceed. This flag won't be checked during an incremental GC anyway,
282 // but it better reflects reality.
283 mReadyForMajorGC
= true;
286 // Subsequent slices should be INTER_SLICE_GC unless they are triggered by
287 // something else that provides its own reason.
288 mMajorGCReason
= JS::GCReason::INTER_SLICE_GC
;
290 MOZ_ASSERT(aEnd
>= aStart
);
291 TimeDuration sliceDuration
= aEnd
- aStart
;
292 PerfStats::RecordMeasurement(PerfStats::Metric::MajorGC
, sliceDuration
);
294 // Compute how much GC time was spent in predicted-to-be-idle time. In the
295 // unlikely event that the slice started after the deadline had already
296 // passed, treat the entire slice as non-idle.
297 TimeDuration nonIdleDuration
;
298 bool startedIdle
= mTriggeredGCDeadline
.isSome() &&
299 !mTriggeredGCDeadline
->IsNull() &&
300 *mTriggeredGCDeadline
> aStart
;
302 nonIdleDuration
= sliceDuration
;
304 if (*mTriggeredGCDeadline
< aEnd
) {
305 // Overran the idle deadline.
306 nonIdleDuration
= aEnd
- *mTriggeredGCDeadline
;
310 PerfStats::RecordMeasurement(PerfStats::Metric::NonIdleMajorGC
,
313 // Note the GC_SLICE_DURING_IDLE previously had a different definition: it was
314 // a histogram of percentages of externally-triggered slices. It is now a
315 // histogram of percentages of all slices. That means that now you might have
316 // a 4ms internal slice (0% during idle) followed by a 16ms external slice
317 // (15ms during idle), whereas before this would show up as a single record of
318 // a single slice with 75% of its time during idle (15 of 20ms).
319 TimeDuration idleDuration
= sliceDuration
- nonIdleDuration
;
321 uint32_t(idleDuration
.ToSeconds() / sliceDuration
.ToSeconds() * 100);
322 Telemetry::Accumulate(Telemetry::GC_SLICE_DURING_IDLE
, percent
);
324 mTriggeredGCDeadline
.reset();
327 void CCGCScheduler::NoteCCBegin(CCReason aReason
, TimeStamp aWhen
,
328 uint32_t aNumForgetSkippables
,
329 uint32_t aSuspected
, uint32_t aRemovedPurples
) {
330 CycleCollectorResults ignoredResults
;
332 "CC", GCCC
, MarkerOptions(MarkerTiming::IntervalStart(aWhen
)),
335 ProfilerString8View::WrapNullTerminatedString(CCReasonToString(aReason
)),
336 aNumForgetSkippables
, aSuspected
, aRemovedPurples
, ignoredResults
,
339 mIsCollectingCycles
= true;
342 void CCGCScheduler::NoteCCEnd(const CycleCollectorResults
& aResults
,
344 mozilla::TimeDuration aMaxSliceTime
) {
345 mCCollectedWaitingForGC
+= aResults
.mFreedGCed
;
346 mCCollectedZonesWaitingForGC
+= aResults
.mFreedJSZones
;
348 PROFILER_MARKER("CC", GCCC
, MarkerOptions(MarkerTiming::IntervalEnd(aWhen
)),
349 CCIntervalMarker
, /* aIsStart */ false, nullptr, 0, 0, 0,
350 aResults
, aMaxSliceTime
);
352 mIsCollectingCycles
= false;
353 mLastCCEndTime
= aWhen
;
354 mNeedsFullCC
= CCReason::NO_REASON
;
357 void CCGCScheduler::NoteWontGC() {
358 mReadyForMajorGC
= !mAskParentBeforeMajorGC
;
359 mMajorGCReason
= JS::GCReason::NO_REASON
;
360 mEagerMajorGCReason
= JS::GCReason::NO_REASON
;
361 mWantAtLeastRegularGC
= false;
362 // Don't clear the WantFullGC state, we will do a full GC the next time a
363 // GC happens for any other reason.
366 bool CCGCScheduler::GCRunnerFired(TimeStamp aDeadline
) {
367 MOZ_ASSERT(!mDidShutdown
, "GCRunner still alive during shutdown");
369 GCRunnerStep step
= GetNextGCRunnerAction(aDeadline
);
370 switch (step
.mAction
) {
371 case GCRunnerAction::None
:
375 case GCRunnerAction::MinorGC
:
376 JS::MaybeRunNurseryCollection(CycleCollectedJSRuntime::Get()->Runtime(),
379 return HasMoreIdleGCRunnerWork();
381 case GCRunnerAction::WaitToMajorGC
: {
382 MOZ_ASSERT(!mHaveAskedParent
, "GCRunner alive after asking the parent");
383 RefPtr
<CCGCScheduler::MayGCPromise
> mbPromise
=
384 CCGCScheduler::MayGCNow(step
.mReason
);
390 mHaveAskedParent
= true;
393 GetMainThreadSerialEventTarget(), __func__
,
394 [this](bool aMayGC
) {
395 mHaveAskedParent
= false;
397 if (!NoteReadyForMajorGC()) {
398 // Another GC started and maybe completed while waiting.
401 // Recreate the GC runner with a 0 delay. The new runner will
402 // continue in idle time.
405 } else if (!InIncrementalGC()) {
406 // We should kill the GC runner since we're done with it, but
407 // only if there's no incremental GC.
412 [this](mozilla::ipc::ResponseRejectReason r
) {
413 mHaveAskedParent
= false;
414 if (!InIncrementalGC()) {
423 case GCRunnerAction::StartMajorGC
:
424 case GCRunnerAction::GCSlice
:
428 return GCRunnerFiredDoGC(aDeadline
, step
);
431 bool CCGCScheduler::GCRunnerFiredDoGC(TimeStamp aDeadline
,
432 const GCRunnerStep
& aStep
) {
433 // Run a GC slice, possibly the first one of a major GC.
434 nsJSContext::IsShrinking is_shrinking
= nsJSContext::NonShrinkingGC
;
435 if (!InIncrementalGC() && aStep
.mReason
== JS::GCReason::USER_INACTIVE
) {
436 bool do_gc
= mWantAtLeastRegularGC
;
438 if (!mUserIsActive
) {
439 if (!nsRefreshDriver::IsRegularRateTimerTicking()) {
440 mIsCompactingOnUserInactive
= true;
441 is_shrinking
= nsJSContext::ShrinkingGC
;
444 // Poke again to restart the timer.
450 using mozilla::ipc::IdleSchedulerChild
;
451 IdleSchedulerChild
* child
=
452 IdleSchedulerChild::GetMainThreadIdleScheduler();
462 // Note that we are triggering the following GC slice and recording whether
463 // it started in idle time, for use in the callback at the end of the slice.
464 mTriggeredGCDeadline
= Some(aDeadline
);
466 MOZ_ASSERT(mActiveIntersliceGCBudget
);
467 TimeStamp startTimeStamp
= TimeStamp::Now();
468 js::SliceBudget budget
= ComputeInterSliceGCBudget(aDeadline
, startTimeStamp
);
469 nsJSContext::RunIncrementalGCSlice(aStep
.mReason
, is_shrinking
, budget
);
471 // If the GC doesn't have any more work to do on the foreground thread (and
472 // e.g. is waiting for background sweeping to finish) then return false to
473 // make IdleTaskRunner postpone the next call a bit.
474 JSContext
* cx
= dom::danger::GetJSContext();
475 return JS::IncrementalGCHasForegroundWork(cx
);
478 RefPtr
<CCGCScheduler::MayGCPromise
> CCGCScheduler::MayGCNow(
479 JS::GCReason reason
) {
480 using namespace mozilla::ipc
;
482 // We ask the parent if we should GC for GCs that aren't too timely,
483 // with the exception of MEM_PRESSURE, in that case we ask the parent
484 // because GCing on too many processes at the same time when under
485 // memory pressure could be a very bad experience for the user.
487 case JS::GCReason::PAGE_HIDE
:
488 case JS::GCReason::MEM_PRESSURE
:
489 case JS::GCReason::USER_INACTIVE
:
490 case JS::GCReason::FULL_GC_TIMER
:
491 case JS::GCReason::CC_FINISHED
: {
492 if (XRE_IsContentProcess()) {
493 IdleSchedulerChild
* child
=
494 IdleSchedulerChild::GetMainThreadIdleScheduler();
496 return child
->MayGCNow();
499 // The parent process doesn't ask IdleSchedulerParent if it can GC.
506 // We use synchronous task dispatch here to avoid a trip through the event
507 // loop if we're on the parent process or it's a GC reason that does not
508 // require permission to GC.
509 RefPtr
<MayGCPromise::Private
> p
= MakeRefPtr
<MayGCPromise::Private
>(__func__
);
510 p
->UseSynchronousTaskDispatch(__func__
);
511 p
->Resolve(true, __func__
);
515 void CCGCScheduler::RunNextCollectorTimer(JS::GCReason aReason
,
516 mozilla::TimeStamp aDeadline
) {
521 // When we're in an incremental GC, we should always have an sGCRunner, so do
522 // not check CC timers. The CC timers won't do anything during a GC.
523 MOZ_ASSERT_IF(InIncrementalGC(), mGCRunner
);
525 RefPtr
<IdleTaskRunner
> runner
;
527 SetWantMajorGC(aReason
);
529 } else if (mCCRunner
) {
534 runner
->SetIdleDeadline(aDeadline
);
539 void CCGCScheduler::PokeShrinkingGC() {
540 if (mShrinkingGCTimer
|| mDidShutdown
) {
544 NS_NewTimerWithFuncCallback(
546 [](nsITimer
* aTimer
, void* aClosure
) {
547 CCGCScheduler
* s
= static_cast<CCGCScheduler
*>(aClosure
);
548 s
->KillShrinkingGCTimer();
549 if (!s
->mUserIsActive
) {
550 if (!nsRefreshDriver::IsRegularRateTimerTicking()) {
551 s
->SetWantMajorGC(JS::GCReason::USER_INACTIVE
);
552 if (!s
->mHaveAskedParent
) {
553 s
->EnsureGCRunner(0);
556 s
->PokeShrinkingGC();
560 this, StaticPrefs::javascript_options_compact_on_user_inactive_delay(),
561 nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY
, "ShrinkingGCTimerFired");
564 void CCGCScheduler::PokeFullGC() {
565 if (!mFullGCTimer
&& !mDidShutdown
) {
566 NS_NewTimerWithFuncCallback(
568 [](nsITimer
* aTimer
, void* aClosure
) {
569 CCGCScheduler
* s
= static_cast<CCGCScheduler
*>(aClosure
);
570 s
->KillFullGCTimer();
572 // Even if the GC is denied by the parent process, because we've
573 // set that we want a full GC we will get one eventually.
575 s
->SetWantMajorGC(JS::GCReason::FULL_GC_TIMER
);
576 if (!s
->mHaveAskedParent
) {
577 s
->EnsureGCRunner(0);
580 this, StaticPrefs::javascript_options_gc_delay_full(),
581 nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY
, "FullGCTimerFired");
585 void CCGCScheduler::PokeGC(JS::GCReason aReason
, JSObject
* aObj
,
586 TimeDuration aDelay
) {
587 MOZ_ASSERT(aReason
!= JS::GCReason::NO_REASON
);
588 MOZ_ASSERT(aReason
!= JS::GCReason::EAGER_NURSERY_COLLECTION
);
594 // If a post-CC GC was pending, then we'll make sure one is happening.
595 mNeedsGCAfterCC
= false;
598 JS::Zone
* zone
= JS::GetObjectZone(aObj
);
599 CycleCollectedJSRuntime::Get()->AddZoneWaitingForGC(zone
);
600 } else if (aReason
!= JS::GCReason::CC_FINISHED
) {
604 if (mGCRunner
|| mHaveAskedParent
) {
605 // There's already a GC runner, or there will be, so just return.
609 SetWantMajorGC(aReason
);
612 // Make sure CC is called regardless of the size of the purple buffer, and
614 EnsureCCThenGC(CCReason::GC_WAITING
);
618 // Wait for javascript.options.gc_delay (or delay_first) then start
619 // looking for idle time to run the initial GC slice.
620 static bool first
= true;
623 : TimeDuration::FromMilliseconds(
624 first
? StaticPrefs::javascript_options_gc_delay_first()
625 : StaticPrefs::javascript_options_gc_delay());
627 EnsureGCRunner(delay
);
630 void CCGCScheduler::PokeMinorGC(JS::GCReason aReason
) {
631 MOZ_ASSERT(aReason
!= JS::GCReason::NO_REASON
);
637 SetWantEagerMinorGC(aReason
);
639 if (mGCRunner
|| mHaveAskedParent
|| mCCRunner
) {
640 // There's already a runner, or there will be, so just return.
644 // Immediately start looking for idle time to run the minor GC.
648 void CCGCScheduler::EnsureOrResetGCRunner() {
654 mGCRunner
->ResetTimer(TimeDuration::FromMilliseconds(
655 StaticPrefs::javascript_options_gc_delay_interslice()));
658 void CCGCScheduler::EnsureGCRunner(TimeDuration aDelay
) {
663 TimeDuration minimumBudget
= TimeDuration::FromMilliseconds(
664 std::max(nsRefreshDriver::HighRateMultiplier() *
665 mActiveIntersliceGCBudget
.ToMilliseconds(),
668 // Wait at most the interslice GC delay before forcing a run.
669 mGCRunner
= IdleTaskRunner::Create(
670 [this](TimeStamp aDeadline
) { return GCRunnerFired(aDeadline
); },
671 "CCGCScheduler::EnsureGCRunner", aDelay
,
672 TimeDuration::FromMilliseconds(
673 StaticPrefs::javascript_options_gc_delay_interslice()),
674 minimumBudget
, true, [this] { return mDidShutdown
; },
676 PROFILER_MARKER_UNTYPED("GC Interrupt", GCCC
);
677 mInterruptRequested
= true;
681 // nsJSEnvironmentObserver observes the user-interaction-inactive notifications
682 // and triggers a shrinking a garbage collection if the user is still inactive
683 // after NS_SHRINKING_GC_DELAY ms later, if the appropriate pref is set.
684 void CCGCScheduler::UserIsInactive() {
685 mUserIsActive
= false;
686 if (StaticPrefs::javascript_options_compact_on_user_inactive()) {
691 void CCGCScheduler::UserIsActive() {
692 mUserIsActive
= true;
693 KillShrinkingGCTimer();
694 if (mIsCompactingOnUserInactive
) {
695 mozilla::dom::AutoJSAPI jsapi
;
697 JS::AbortIncrementalGC(jsapi
.cx());
699 MOZ_ASSERT(!mIsCompactingOnUserInactive
);
702 void CCGCScheduler::KillShrinkingGCTimer() {
703 if (mShrinkingGCTimer
) {
704 mShrinkingGCTimer
->Cancel();
705 NS_RELEASE(mShrinkingGCTimer
);
709 void CCGCScheduler::KillFullGCTimer() {
711 mFullGCTimer
->Cancel();
712 NS_RELEASE(mFullGCTimer
);
716 void CCGCScheduler::KillGCRunner() {
717 // If we're in an incremental GC then killing the timer is only okay if
718 // we're shutting down.
719 MOZ_ASSERT(!(InIncrementalGC() && !mDidShutdown
));
726 void CCGCScheduler::EnsureCCRunner(TimeDuration aDelay
, TimeDuration aBudget
) {
727 MOZ_ASSERT(!mDidShutdown
);
729 TimeDuration minimumBudget
= TimeDuration::FromMilliseconds(std::max(
730 nsRefreshDriver::HighRateMultiplier() * aBudget
.ToMilliseconds(), 1.0));
733 mCCRunner
= IdleTaskRunner::Create(
734 CCRunnerFired
, "EnsureCCRunner::CCRunnerFired", 0, aDelay
,
735 minimumBudget
, true, [this] { return mDidShutdown
; });
737 mCCRunner
->SetMinimumUsefulBudget(minimumBudget
.ToMilliseconds());
738 nsIEventTarget
* target
= mozilla::GetCurrentSerialEventTarget();
740 mCCRunner
->SetTimer(aDelay
, target
);
745 void CCGCScheduler::MaybePokeCC(TimeStamp aNow
, uint32_t aSuspectedCCObjects
) {
746 if (mCCRunner
|| mDidShutdown
) {
750 CCReason reason
= ShouldScheduleCC(aNow
, aSuspectedCCObjects
);
751 if (reason
!= CCReason::NO_REASON
) {
752 // We can kill some objects before running forgetSkippable.
753 nsCycleCollector_dispatchDeferredDeletion();
756 InitCCRunnerStateMachine(CCRunnerState::ReducePurple
, reason
);
758 EnsureCCRunner(kCCSkippableDelay
, kForgetSkippableSliceDuration
);
762 void CCGCScheduler::KillCCRunner() {
764 DeactivateCCRunner();
771 void CCGCScheduler::KillAllTimersAndRunners() {
772 KillShrinkingGCTimer();
778 js::SliceBudget
CCGCScheduler::ComputeCCSliceBudget(
779 TimeStamp aDeadline
, TimeStamp aCCBeginTime
, TimeStamp aPrevSliceEndTime
,
780 TimeStamp aNow
, bool* aPreferShorterSlices
) const {
781 *aPreferShorterSlices
=
782 aDeadline
.IsNull() || (aDeadline
- aNow
) < kICCSliceBudget
;
784 TimeDuration baseBudget
=
785 aDeadline
.IsNull() ? kICCSliceBudget
: aDeadline
- aNow
;
787 if (aPrevSliceEndTime
.IsNull()) {
788 // The first slice gets the standard slice time.
789 return js::SliceBudget(js::TimeBudget(baseBudget
));
792 // Only run a limited slice if we're within the max running time.
793 MOZ_ASSERT(aNow
>= aCCBeginTime
);
794 TimeDuration runningTime
= aNow
- aCCBeginTime
;
795 if (runningTime
>= kMaxICCDuration
) {
796 return js::SliceBudget::unlimited();
799 const TimeDuration maxSlice
=
800 TimeDuration::FromMilliseconds(MainThreadIdlePeriod::GetLongIdlePeriod());
802 // Try to make up for a delay in running this slice.
803 MOZ_ASSERT(aNow
>= aPrevSliceEndTime
);
804 double sliceDelayMultiplier
=
805 (aNow
- aPrevSliceEndTime
) / kICCIntersliceDelay
;
806 TimeDuration delaySliceBudget
=
807 std::min(baseBudget
.MultDouble(sliceDelayMultiplier
), maxSlice
);
809 // Increase slice budgets up to |maxSlice| as we approach
810 // half way through the ICC, to avoid large sync CCs.
811 double percentToHalfDone
=
812 std::min(2.0 * (runningTime
/ kMaxICCDuration
), 1.0);
813 TimeDuration laterSliceBudget
= maxSlice
.MultDouble(percentToHalfDone
);
815 // Note: We may have already overshot the deadline, in which case
816 // baseBudget will be negative and we will end up returning
818 return js::SliceBudget(js::TimeBudget(
819 std::max({delaySliceBudget
, laterSliceBudget
, baseBudget
})));
822 js::SliceBudget
CCGCScheduler::ComputeInterSliceGCBudget(TimeStamp aDeadline
,
824 // We use longer budgets when the CC has been locked out but the CC has
825 // tried to run since that means we may have a significant amount of
826 // garbage to collect and it's better to GC in several longer slices than
827 // in a very long one.
828 TimeDuration budget
=
829 aDeadline
.IsNull() ? mActiveIntersliceGCBudget
* 2 : aDeadline
- aNow
;
830 if (!mCCBlockStart
) {
831 return CreateGCSliceBudget(budget
, !aDeadline
.IsNull(), false);
834 TimeDuration blockedTime
= aNow
- mCCBlockStart
;
835 TimeDuration maxSliceGCBudget
= mActiveIntersliceGCBudget
* 10;
836 double percentOfBlockedTime
=
837 std::min(blockedTime
/ kMaxCCLockedoutTime
, 1.0);
838 TimeDuration extendedBudget
=
839 maxSliceGCBudget
.MultDouble(percentOfBlockedTime
);
840 if (budget
>= extendedBudget
) {
841 return CreateGCSliceBudget(budget
, !aDeadline
.IsNull(), false);
844 // If the budget is being extended, do not allow it to be interrupted.
845 auto result
= js::SliceBudget(js::TimeBudget(extendedBudget
), nullptr);
846 result
.idle
= !aDeadline
.IsNull();
847 result
.extended
= true;
851 CCReason
CCGCScheduler::ShouldScheduleCC(TimeStamp aNow
,
852 uint32_t aSuspectedCCObjects
) const {
854 return CCReason::NO_REASON
;
857 // Don't run consecutive CCs too often.
858 if (mCleanupsSinceLastGC
&& !mLastCCEndTime
.IsNull()) {
859 if (aNow
- mLastCCEndTime
< kCCDelay
) {
860 return CCReason::NO_REASON
;
864 // If GC hasn't run recently and forget skippable only cycle was run,
865 // don't start a new cycle too soon.
866 if ((mCleanupsSinceLastGC
> kMajorForgetSkippableCalls
) &&
867 !mLastForgetSkippableCycleEndTime
.IsNull()) {
868 if (aNow
- mLastForgetSkippableCycleEndTime
<
869 kTimeBetweenForgetSkippableCycles
) {
870 return CCReason::NO_REASON
;
874 return IsCCNeeded(aNow
, aSuspectedCCObjects
);
877 CCRunnerStep
CCGCScheduler::AdvanceCCRunner(TimeStamp aDeadline
, TimeStamp aNow
,
878 uint32_t aSuspectedCCObjects
) {
879 struct StateDescriptor
{
880 // When in this state, should we first check to see if we still have
881 // enough reason to CC?
884 // If we do decide to abort the CC, should we still try to forget
885 // skippables one more time?
886 bool mTryFinalForgetSkippable
;
889 // The state descriptors for Inactive and Canceled will never actually be
890 // used. We will never call this function while Inactive, and Canceled is
891 // handled specially at the beginning.
892 constexpr StateDescriptor stateDescriptors
[] = {
893 {false, false}, /* CCRunnerState::Inactive */
894 {false, false}, /* CCRunnerState::ReducePurple */
895 {true, true}, /* CCRunnerState::CleanupChildless */
896 {true, false}, /* CCRunnerState::CleanupContentUnbinder */
897 {false, false}, /* CCRunnerState::CleanupDeferred */
898 {false, false}, /* CCRunnerState::StartCycleCollection */
899 {false, false}, /* CCRunnerState::CycleCollecting */
900 {false, false}}; /* CCRunnerState::Canceled */
902 ArrayLength(stateDescriptors
) == size_t(CCRunnerState::NumStates
),
903 "need one state descriptor per state");
904 const StateDescriptor
& desc
= stateDescriptors
[int(mCCRunnerState
)];
906 // Make sure we initialized the state machine.
907 MOZ_ASSERT(mCCRunnerState
!= CCRunnerState::Inactive
);
910 return {CCRunnerAction::StopRunning
, Yield
};
913 if (mCCRunnerState
== CCRunnerState::Canceled
) {
914 // When we cancel a cycle, there may have been a final ForgetSkippable.
915 return {CCRunnerAction::StopRunning
, Yield
};
918 if (InIncrementalGC()) {
919 if (mCCBlockStart
.IsNull()) {
922 // If we have reached the CycleCollecting state, then ignore CC timer
923 // fires while incremental GC is running. (Running ICC during an IGC
924 // would cause us to synchronously finish the GC, which is bad.)
926 // If we have not yet started cycle collecting, then reset our state so
927 // that we run forgetSkippable often enough before CC. Because of reduced
928 // mCCDelay, forgetSkippable will be called just a few times.
930 // The kMaxCCLockedoutTime limit guarantees that we end up calling
931 // forgetSkippable and CycleCollectNow eventually.
933 if (mCCRunnerState
!= CCRunnerState::CycleCollecting
) {
934 mCCRunnerState
= CCRunnerState::ReducePurple
;
935 mCCRunnerEarlyFireCount
= 0;
936 mCCDelay
= kCCDelay
/ int64_t(3);
938 return {CCRunnerAction::None
, Yield
};
941 if (GetCCBlockedTime(aNow
) < kMaxCCLockedoutTime
) {
942 return {CCRunnerAction::None
, Yield
};
945 // Locked out for too long, so proceed and finish the incremental GC
949 // For states that aren't just continuations of previous states, check
950 // whether a CC is still needed (after doing various things to reduce the
952 if (desc
.mCanAbortCC
&&
953 IsCCNeeded(aNow
, aSuspectedCCObjects
) == CCReason::NO_REASON
) {
954 // If we don't pass the threshold for wanting to cycle collect, stop now
955 // (after possibly doing a final ForgetSkippable).
956 mCCRunnerState
= CCRunnerState::Canceled
;
957 NoteForgetSkippableOnlyCycle(aNow
);
959 // Preserve the previous code's idea of when to check whether a
960 // ForgetSkippable should be fired.
961 if (desc
.mTryFinalForgetSkippable
&&
962 ShouldForgetSkippable(aSuspectedCCObjects
)) {
963 // The Canceled state will make us StopRunning after this action is
964 // performed (see conditional at top of function).
965 return {CCRunnerAction::ForgetSkippable
, Yield
, KeepChildless
};
968 return {CCRunnerAction::StopRunning
, Yield
};
971 if (mEagerMinorGCReason
!= JS::GCReason::NO_REASON
&& !aDeadline
.IsNull()) {
972 return {CCRunnerAction::MinorGC
, Continue
, mEagerMinorGCReason
};
975 switch (mCCRunnerState
) {
976 // ReducePurple: a GC ran (or we otherwise decided to try CC'ing). Wait
977 // for some amount of time (kCCDelay, or less if incremental GC blocked
978 // this CC) while firing regular ForgetSkippable actions before continuing
980 case CCRunnerState::ReducePurple
:
981 ++mCCRunnerEarlyFireCount
;
982 if (IsLastEarlyCCTimer(mCCRunnerEarlyFireCount
)) {
983 mCCRunnerState
= CCRunnerState::CleanupChildless
;
986 if (ShouldForgetSkippable(aSuspectedCCObjects
)) {
987 return {CCRunnerAction::ForgetSkippable
, Yield
, KeepChildless
};
990 if (aDeadline
.IsNull()) {
991 return {CCRunnerAction::None
, Yield
};
994 // If we're called during idle time, try to find some work to do by
995 // advancing to the next state, effectively bypassing some possible forget
997 mCCRunnerState
= CCRunnerState::CleanupChildless
;
999 // Continue on to CleanupChildless, but only after checking IsCCNeeded
1001 return {CCRunnerAction::None
, Continue
};
1003 // CleanupChildless: do a stronger ForgetSkippable that removes nodes with
1004 // no children in the cycle collector graph. This state is split into 3
1005 // parts; the other Cleanup* actions will happen within the same callback
1006 // (unless the ForgetSkippable shrinks the purple buffer enough for the CC
1007 // to be skipped entirely.)
1008 case CCRunnerState::CleanupChildless
:
1009 mCCRunnerState
= CCRunnerState::CleanupContentUnbinder
;
1010 return {CCRunnerAction::ForgetSkippable
, Yield
, RemoveChildless
};
1012 // CleanupContentUnbinder: continuing cleanup, clear out the content
1014 case CCRunnerState::CleanupContentUnbinder
:
1015 if (aDeadline
.IsNull()) {
1016 // Non-idle (waiting) callbacks skip the rest of the cleanup, but still
1017 // wait for another fire before the actual CC.
1018 mCCRunnerState
= CCRunnerState::StartCycleCollection
;
1019 return {CCRunnerAction::None
, Yield
};
1022 // Running in an idle callback.
1024 // The deadline passed, so go straight to CC in the next slice.
1025 if (aNow
>= aDeadline
) {
1026 mCCRunnerState
= CCRunnerState::StartCycleCollection
;
1027 return {CCRunnerAction::None
, Yield
};
1030 mCCRunnerState
= CCRunnerState::CleanupDeferred
;
1031 return {CCRunnerAction::CleanupContentUnbinder
, Continue
};
1033 // CleanupDeferred: continuing cleanup, do deferred deletion.
1034 case CCRunnerState::CleanupDeferred
:
1035 MOZ_ASSERT(!aDeadline
.IsNull(),
1036 "Should only be in CleanupDeferred state when idle");
1038 // Our efforts to avoid a CC have failed. Let the timer fire once more
1040 mCCRunnerState
= CCRunnerState::StartCycleCollection
;
1041 if (aNow
>= aDeadline
) {
1042 // The deadline passed, go straight to CC in the next slice.
1043 return {CCRunnerAction::None
, Yield
};
1046 return {CCRunnerAction::CleanupDeferred
, Yield
};
1048 // StartCycleCollection: start actually doing cycle collection slices.
1049 case CCRunnerState::StartCycleCollection
:
1050 // We are in the final timer fire and still meet the conditions for
1051 // triggering a CC. Let RunCycleCollectorSlice finish the current IGC if
1052 // any, because that will allow us to include the GC time in the CC pause.
1053 mCCRunnerState
= CCRunnerState::CycleCollecting
;
1056 // CycleCollecting: continue running slices until done.
1057 case CCRunnerState::CycleCollecting
: {
1058 CCRunnerStep step
{CCRunnerAction::CycleCollect
, Yield
};
1059 step
.mParam
.mCCReason
= mCCReason
;
1060 mCCReason
= CCReason::SLICE
; // Set reason for following slices.
1065 MOZ_CRASH("Unexpected CCRunner state");
1069 GCRunnerStep
CCGCScheduler::GetNextGCRunnerAction(TimeStamp aDeadline
) const {
1070 if (InIncrementalGC()) {
1071 MOZ_ASSERT(mMajorGCReason
!= JS::GCReason::NO_REASON
);
1072 return {GCRunnerAction::GCSlice
, mMajorGCReason
};
1075 // Service a non-eager GC request first, even if it requires waiting.
1076 if (mMajorGCReason
!= JS::GCReason::NO_REASON
) {
1077 return {mReadyForMajorGC
? GCRunnerAction::StartMajorGC
1078 : GCRunnerAction::WaitToMajorGC
,
1082 // Now for eager requests, which are ignored unless we're idle.
1083 if (!aDeadline
.IsNull()) {
1084 if (mEagerMajorGCReason
!= JS::GCReason::NO_REASON
) {
1085 return {mReadyForMajorGC
? GCRunnerAction::StartMajorGC
1086 : GCRunnerAction::WaitToMajorGC
,
1087 mEagerMajorGCReason
};
1090 if (mEagerMinorGCReason
!= JS::GCReason::NO_REASON
) {
1091 return {GCRunnerAction::MinorGC
, mEagerMinorGCReason
};
1095 return {GCRunnerAction::None
, JS::GCReason::NO_REASON
};
1098 js::SliceBudget
CCGCScheduler::ComputeForgetSkippableBudget(
1099 TimeStamp aStartTimeStamp
, TimeStamp aDeadline
) {
1100 if (mForgetSkippableFrequencyStartTime
.IsNull()) {
1101 mForgetSkippableFrequencyStartTime
= aStartTimeStamp
;
1102 } else if (aStartTimeStamp
- mForgetSkippableFrequencyStartTime
>
1104 TimeStamp startPlusMinute
= mForgetSkippableFrequencyStartTime
+ kOneMinute
;
1106 // If we had forget skippables only at the beginning of the interval, we
1107 // still want to use the whole time, minute or more, for frequency
1108 // calculation. mLastForgetSkippableEndTime is needed if forget skippable
1109 // takes enough time to push the interval to be over a minute.
1110 TimeStamp endPoint
= std::max(startPlusMinute
, mLastForgetSkippableEndTime
);
1112 // Duration in minutes.
1114 (endPoint
- mForgetSkippableFrequencyStartTime
).ToSeconds() / 60;
1115 uint32_t frequencyPerMinute
= uint32_t(mForgetSkippableCounter
/ duration
);
1116 Telemetry::Accumulate(Telemetry::FORGET_SKIPPABLE_FREQUENCY
,
1117 frequencyPerMinute
);
1118 mForgetSkippableCounter
= 0;
1119 mForgetSkippableFrequencyStartTime
= aStartTimeStamp
;
1121 ++mForgetSkippableCounter
;
1123 TimeDuration budgetTime
=
1124 aDeadline
? (aDeadline
- aStartTimeStamp
) : kForgetSkippableSliceDuration
;
1125 return js::SliceBudget(budgetTime
);
1128 } // namespace mozilla