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/. */
4 
5 #include "CCGCScheduler.h"
6 
7 #include "mozilla/StaticPrefs_javascript.h"
8 #include "mozilla/CycleCollectedJSRuntime.h"
9 #include "mozilla/ProfilerMarkers.h"
10 #include "mozilla/dom/ScriptSettings.h"
11 #include "nsRefreshDriver.h"
12 
13 /*
14  * GC Scheduling from Firefox
15  * ==========================
16  *
17  * See also GC Scheduling from SpiderMonkey's perspective here:
18  * https://searchfox.org/mozilla-central/source/js/src/gc/Scheduling.h
19  *
20  * From Firefox's perspective GCs can start in 5 different ways:
21  *
22  *  * The JS engine just starts doing a GC for its own reasons (see above).
23  *    Firefox finds out about these via a callback in nsJSEnvironment.cpp
24  *  * PokeGC()
25  *  * PokeFullGC()
26  *  * PokeShrinkingGC()
27  *  * memory-pressure GCs (via a listener in nsJSEnvironment.cpp).
28  *
29  * PokeGC
30  * ------
31  *
32  * void CCGCScheduler::PokeGC(JS::GCReason aReason, JSObject* aObj,
33  *                            TimeDuration aDelay)
34  *
35  * PokeGC provides a way for callers to say "Hey, there may be some memory
36  * associated with this object (via Zone) you can collect."  PokeGC will:
37  *  * add the zone to a set,
38  *  * set flags including what kind of GC to run (SetWantMajorGC),
39  *  * then creates the mGCRunner with a short delay.
40  *
41  * The delay can allow other calls to PokeGC to add their zones so they can
42  * be collected together.
43  *
44  * See below for what happens when mGCRunner fires.
45  *
46  * PokeFullGC
47  * ----------
48  *
49  * void CCGCScheduler::PokeFullGC()
50  *
51  * PokeFullGC will create a timer that will initiate a "full" (all zones)
52  * collection.  This is usually used after a regular collection if a full GC
53  * seems like a good idea (to collect inter-zone references).
54  *
55  * When the timer fires it will:
56  *  * set flags (SetWantMajorGC),
57  *  * start the mGCRunner with zero delay.
58  *
59  * See below for when mGCRunner fires.
60  *
61  * PokeShrinkingGC
62  * ---------------
63  *
64  * void CCGCScheduler::PokeShrinkingGC()
65  *
66  * PokeShrinkingGC is called when Firefox's user is inactive.
67  * Like PokeFullGC, PokeShrinkingGC uses a timer, but the timeout is longer
68  * which should prevent the ShrinkingGC from starting if the user only
69  * glances away for a brief time.  When the timer fires it will:
70  *
71  *  * set flags (SetWantMajorGC),
72  *  * create the mGCRunner.
73  *
74  * There is a check if the user is still inactive in GCRunnerFired), if the
75  * user has become active the shrinking GC is canceled and either a regular
76  * GC (if requested, see mWantAtLeastRegularGC) or no GC is run.
77  *
78  * When mGCRunner fires
79  * --------------------
80  *
81  * When mGCRunner fires it calls GCRunnerFired.  This starts in the
82  * WaitToMajorGC state:
83  *
84  *  * If this is a parent process it jumps to the next state
85  *  * If this is a content process it will ask the parent if now is a good
86  *      time to do a GC.  (MayGCNow)
87  *  * kill the mGCRunner
88  *  * Exit
89  *
90  * Meanwhile the parent process will queue GC requests so that not too many
91  * are running in parallel overwhelming the CPU cores (see
92  * IdleSchedulerParent).
93  *
94  * When the promise from MayGCNow is resolved it will set some
95  * state (NoteReadyForMajorGC) and restore the mGCRunner.
96  *
97  * When the mGCRunner runs a second time (or this is the parent process and
98  * which jumped over the above logic.  It will be in the StartMajorGC state.
99  * It will initiate the GC for real, usually.  If it's a shrinking GC and the
100  * user is now active again it may abort.  See GCRunnerFiredDoGC().
101  *
102  * The runner will then run the first slice of the garbage collection.
103  * Later slices are also run by the runner, the final slice kills the runner
104  * from the GC callback in nsJSEnvironment.cpp.
105  *
106  * There is additional logic in the code to handle concurrent requests of
107  * various kinds.
108  */
109 
110 namespace mozilla {
111 
NoteGCBegin()112 void CCGCScheduler::NoteGCBegin() {
113   // Treat all GC as incremental here; non-incremental GC will just appear to
114   // be one slice.
115   mInIncrementalGC = true;
116   mReadyForMajorGC = !mAskParentBeforeMajorGC;
117 
118   // Tell the parent process that we've started a GC (it might not know if
119   // we hit a threshold in the JS engine).
120   using mozilla::ipc::IdleSchedulerChild;
121   IdleSchedulerChild* child = IdleSchedulerChild::GetMainThreadIdleScheduler();
122   if (child) {
123     child->StartedGC();
124   }
125 }
126 
NoteGCEnd()127 void CCGCScheduler::NoteGCEnd() {
128   mMajorGCReason = JS::GCReason::NO_REASON;
129 
130   mInIncrementalGC = false;
131   mCCBlockStart = TimeStamp();
132   mReadyForMajorGC = !mAskParentBeforeMajorGC;
133   mWantAtLeastRegularGC = false;
134   mNeedsFullCC = CCReason::GC_FINISHED;
135   mHasRunGC = true;
136   mIsCompactingOnUserInactive = false;
137 
138   mCleanupsSinceLastGC = 0;
139   mCCollectedWaitingForGC = 0;
140   mCCollectedZonesWaitingForGC = 0;
141   mLikelyShortLivingObjectsNeedingGC = 0;
142 
143   using mozilla::ipc::IdleSchedulerChild;
144   IdleSchedulerChild* child = IdleSchedulerChild::GetMainThreadIdleScheduler();
145   if (child) {
146     child->DoneGC();
147   }
148 }
149 
150 #ifdef MOZ_GECKO_PROFILER
151 struct CCIntervalMarker {
MarkerTypeNamemozilla::CCIntervalMarker152   static constexpr mozilla::Span<const char> MarkerTypeName() {
153     return mozilla::MakeStringSpan("CC");
154   }
StreamJSONMarkerDatamozilla::CCIntervalMarker155   static void StreamJSONMarkerData(
156       baseprofiler::SpliceableJSONWriter& aWriter,
157       const mozilla::ProfilerString8View& aReason) {
158     if (aReason.Length()) {
159       aWriter.StringProperty("reason", aReason);
160     }
161   }
MarkerTypeDisplaymozilla::CCIntervalMarker162   static mozilla::MarkerSchema MarkerTypeDisplay() {
163     using MS = mozilla::MarkerSchema;
164     MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable,
165               MS::Location::TimelineMemory};
166     schema.AddStaticLabelValue(
167         "Description",
168         "Summary data for the core part of a cycle collection, possibly "
169         "encompassing a set of incremental slices. The main thread is not "
170         "blocked for the entire major CC interval, only for the individual "
171         "slices.");
172     schema.AddKeyLabelFormatSearchable("reason", "Reason", MS::Format::String,
173                                        MS::Searchable::Searchable);
174     return schema;
175   }
176 };
177 #endif
178 
NoteCCBegin(CCReason aReason,TimeStamp aWhen)179 void CCGCScheduler::NoteCCBegin(CCReason aReason, TimeStamp aWhen) {
180 #ifdef MOZ_GECKO_PROFILER
181   profiler_add_marker(
182       "CC", baseprofiler::category::GCCC,
183       MarkerOptions(MarkerTiming::IntervalStart(aWhen)), CCIntervalMarker{},
184       ProfilerString8View::WrapNullTerminatedString(CCReasonToString(aReason)));
185 #endif
186 
187   mIsCollectingCycles = true;
188 }
189 
NoteCCEnd(TimeStamp aWhen)190 void CCGCScheduler::NoteCCEnd(TimeStamp aWhen) {
191 #ifdef MOZ_GECKO_PROFILER
192   profiler_add_marker("CC", baseprofiler::category::GCCC,
193                       MarkerOptions(MarkerTiming::IntervalEnd(aWhen)),
194                       CCIntervalMarker{}, nullptr);
195 #endif
196 
197   mIsCollectingCycles = false;
198   mLastCCEndTime = aWhen;
199   mNeedsFullCC = CCReason::NO_REASON;
200 
201   // The GC for this CC has already been requested.
202   mNeedsGCAfterCC = false;
203 }
204 
NoteWontGC()205 void CCGCScheduler::NoteWontGC() {
206   mReadyForMajorGC = !mAskParentBeforeMajorGC;
207   mMajorGCReason = JS::GCReason::NO_REASON;
208   mWantAtLeastRegularGC = false;
209   // Don't clear the WantFullGC state, we will do a full GC the next time a
210   // GC happens for any other reason.
211 }
212 
GCRunnerFired(TimeStamp aDeadline)213 bool CCGCScheduler::GCRunnerFired(TimeStamp aDeadline) {
214   MOZ_ASSERT(!mDidShutdown, "GCRunner still alive during shutdown");
215 
216   GCRunnerStep step = GetNextGCRunnerAction();
217   switch (step.mAction) {
218     case GCRunnerAction::None:
219       MOZ_CRASH("Unexpected GCRunnerAction");
220 
221     case GCRunnerAction::WaitToMajorGC: {
222       MOZ_ASSERT(!mHaveAskedParent, "GCRunner alive after asking the parent");
223       RefPtr<CCGCScheduler::MayGCPromise> mbPromise =
224           CCGCScheduler::MayGCNow(step.mReason);
225       if (!mbPromise) {
226         // We can GC now.
227         break;
228       }
229 
230       mHaveAskedParent = true;
231       KillGCRunner();
232       mbPromise->Then(
233           GetMainThreadSerialEventTarget(), __func__,
234           [this](bool aMayGC) {
235             mHaveAskedParent = false;
236             if (aMayGC) {
237               if (!NoteReadyForMajorGC()) {
238                 // Another GC started and maybe completed while waiting.
239                 return;
240               }
241               // Recreate the GC runner with a 0 delay.  The new runner will
242               // continue in idle time.
243               KillGCRunner();
244               EnsureGCRunner(0);
245             } else if (!InIncrementalGC()) {
246               // We should kill the GC runner since we're done with it, but
247               // only if there's no incremental GC.
248               KillGCRunner();
249               NoteWontGC();
250             }
251           },
252           [this](mozilla::ipc::ResponseRejectReason r) {
253             mHaveAskedParent = false;
254             if (!InIncrementalGC()) {
255               KillGCRunner();
256               NoteWontGC();
257             }
258           });
259 
260       return true;
261     }
262 
263     case GCRunnerAction::StartMajorGC:
264     case GCRunnerAction::GCSlice:
265       break;
266   }
267 
268   return GCRunnerFiredDoGC(aDeadline, step);
269 }
270 
GCRunnerFiredDoGC(TimeStamp aDeadline,const GCRunnerStep & aStep)271 bool CCGCScheduler::GCRunnerFiredDoGC(TimeStamp aDeadline,
272                                       const GCRunnerStep& aStep) {
273   // Run a GC slice, possibly the first one of a major GC.
274   nsJSContext::IsShrinking is_shrinking = nsJSContext::NonShrinkingGC;
275   if (!InIncrementalGC() && aStep.mReason == JS::GCReason::USER_INACTIVE) {
276     bool do_gc = mWantAtLeastRegularGC;
277 
278     if (!mUserIsActive) {
279       if (!nsRefreshDriver::IsRegularRateTimerTicking()) {
280         mIsCompactingOnUserInactive = true;
281         is_shrinking = nsJSContext::ShrinkingGC;
282         do_gc = true;
283       } else {
284         // Poke again to restart the timer.
285         PokeShrinkingGC();
286       }
287     }
288 
289     if (!do_gc) {
290       using mozilla::ipc::IdleSchedulerChild;
291       IdleSchedulerChild* child =
292           IdleSchedulerChild::GetMainThreadIdleScheduler();
293       if (child) {
294         child->DoneGC();
295       }
296       NoteWontGC();
297       KillGCRunner();
298       return true;
299     }
300   }
301 
302   MOZ_ASSERT(mActiveIntersliceGCBudget);
303   TimeStamp startTimeStamp = TimeStamp::Now();
304   js::SliceBudget budget = ComputeInterSliceGCBudget(aDeadline, startTimeStamp);
305   TimeDuration duration = mGCUnnotifiedTotalTime;
306   nsJSContext::RunIncrementalGCSlice(aStep.mReason, is_shrinking, budget);
307 
308   mGCUnnotifiedTotalTime = TimeDuration();
309   TimeStamp now = TimeStamp::Now();
310   TimeDuration sliceDuration = now - startTimeStamp;
311   duration += sliceDuration;
312   if (duration.ToSeconds()) {
313     TimeDuration idleDuration;
314     if (!aDeadline.IsNull()) {
315       if (aDeadline < now) {
316         // This slice overflowed the idle period.
317         if (aDeadline > startTimeStamp) {
318           idleDuration = aDeadline - startTimeStamp;
319         }
320         // Otherwise the whole slice was done outside the idle period.
321       } else {
322         // Note, we don't want to use duration here, since it may contain
323         // data also from JS engine triggered GC slices.
324         idleDuration = sliceDuration;
325       }
326     }
327 
328     uint32_t percent =
329         uint32_t(idleDuration.ToSeconds() / duration.ToSeconds() * 100);
330     Telemetry::Accumulate(Telemetry::GC_SLICE_DURING_IDLE, percent);
331   }
332 
333   // If the GC doesn't have any more work to do on the foreground thread (and
334   // e.g. is waiting for background sweeping to finish) then return false to
335   // make IdleTaskRunner postpone the next call a bit.
336   JSContext* cx = dom::danger::GetJSContext();
337   return JS::IncrementalGCHasForegroundWork(cx);
338 }
339 
MayGCNow(JS::GCReason reason)340 RefPtr<CCGCScheduler::MayGCPromise> CCGCScheduler::MayGCNow(
341     JS::GCReason reason) {
342   using namespace mozilla::ipc;
343 
344   // We ask the parent if we should GC for GCs that aren't too timely,
345   // with the exception of MEM_PRESSURE, in that case we ask the parent
346   // because GCing on too many processes at the same time when under
347   // memory pressure could be a very bad experience for the user.
348   switch (reason) {
349     case JS::GCReason::PAGE_HIDE:
350     case JS::GCReason::MEM_PRESSURE:
351     case JS::GCReason::USER_INACTIVE:
352     case JS::GCReason::FULL_GC_TIMER:
353     case JS::GCReason::CC_FINISHED: {
354       if (XRE_IsContentProcess()) {
355         IdleSchedulerChild* child =
356             IdleSchedulerChild::GetMainThreadIdleScheduler();
357         if (child) {
358           return child->MayGCNow();
359         }
360       }
361       // The parent process doesn't ask IdleSchedulerParent if it can GC.
362       break;
363     }
364     default:
365       break;
366   }
367 
368   // We use synchronous task dispatch here to avoid a trip through the event
369   // loop if we're on the parent process or it's a GC reason that does not
370   // require permission to GC.
371   RefPtr<MayGCPromise::Private> p = MakeRefPtr<MayGCPromise::Private>(__func__);
372   p->UseSynchronousTaskDispatch(__func__);
373   p->Resolve(true, __func__);
374   return p;
375 }
376 
RunNextCollectorTimer(JS::GCReason aReason,mozilla::TimeStamp aDeadline)377 void CCGCScheduler::RunNextCollectorTimer(JS::GCReason aReason,
378                                           mozilla::TimeStamp aDeadline) {
379   if (mDidShutdown) {
380     return;
381   }
382 
383   // When we're in an incremental GC, we should always have an sGCRunner, so do
384   // not check CC timers. The CC timers won't do anything during a GC.
385   MOZ_ASSERT_IF(InIncrementalGC(), mGCRunner);
386 
387   RefPtr<IdleTaskRunner> runner;
388   if (mGCRunner) {
389     SetWantMajorGC(aReason);
390     runner = mGCRunner;
391   } else if (mCCRunner) {
392     runner = mCCRunner;
393   }
394 
395   if (runner) {
396     runner->SetIdleDeadline(aDeadline);
397     runner->Run();
398   }
399 }
400 
PokeShrinkingGC()401 void CCGCScheduler::PokeShrinkingGC() {
402   if (mShrinkingGCTimer || mDidShutdown) {
403     return;
404   }
405 
406   NS_NewTimerWithFuncCallback(
407       &mShrinkingGCTimer,
408       [](nsITimer* aTimer, void* aClosure) {
409         CCGCScheduler* s = static_cast<CCGCScheduler*>(aClosure);
410         s->KillShrinkingGCTimer();
411         if (!s->mUserIsActive) {
412           if (!nsRefreshDriver::IsRegularRateTimerTicking()) {
413             s->SetWantMajorGC(JS::GCReason::USER_INACTIVE);
414             if (!s->mHaveAskedParent) {
415               s->EnsureGCRunner(0);
416             }
417           } else {
418             s->PokeShrinkingGC();
419           }
420         }
421       },
422       this, StaticPrefs::javascript_options_compact_on_user_inactive_delay(),
423       nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY, "ShrinkingGCTimerFired");
424 }
425 
PokeFullGC()426 void CCGCScheduler::PokeFullGC() {
427   if (!mFullGCTimer && !mDidShutdown) {
428     NS_NewTimerWithFuncCallback(
429         &mFullGCTimer,
430         [](nsITimer* aTimer, void* aClosure) {
431           CCGCScheduler* s = static_cast<CCGCScheduler*>(aClosure);
432           s->KillFullGCTimer();
433 
434           // Even if the GC is denied by the parent process, because we've
435           // set that we want a full GC we will get one eventually.
436           s->SetNeedsFullGC();
437           s->SetWantMajorGC(JS::GCReason::FULL_GC_TIMER);
438           if (!s->mHaveAskedParent) {
439             s->EnsureGCRunner(0);
440           }
441         },
442         this, StaticPrefs::javascript_options_gc_delay_full(),
443         nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY, "FullGCTimerFired");
444   }
445 }
446 
PokeGC(JS::GCReason aReason,JSObject * aObj,TimeDuration aDelay)447 void CCGCScheduler::PokeGC(JS::GCReason aReason, JSObject* aObj,
448                            TimeDuration aDelay) {
449   if (mDidShutdown) {
450     return;
451   }
452 
453   if (aObj) {
454     JS::Zone* zone = JS::GetObjectZone(aObj);
455     CycleCollectedJSRuntime::Get()->AddZoneWaitingForGC(zone);
456   } else if (aReason != JS::GCReason::CC_FINISHED) {
457     SetNeedsFullGC();
458   }
459 
460   if (mGCRunner || mHaveAskedParent) {
461     // There's already a GC runner, there or will be, so just return.
462     return;
463   }
464 
465   SetWantMajorGC(aReason);
466 
467   if (mCCRunner) {
468     // Make sure CC is called regardless of the size of the purple buffer, and
469     // GC after it.
470     EnsureCCThenGC(CCReason::GC_WAITING);
471     return;
472   }
473 
474   // Wait for javascript.options.gc_delay (or delay_first) then start
475   // looking for idle time to run the initial GC slice.
476   static bool first = true;
477   TimeDuration delay =
478       aDelay ? aDelay
479              : TimeDuration::FromMilliseconds(
480                    first ? StaticPrefs::javascript_options_gc_delay_first()
481                          : StaticPrefs::javascript_options_gc_delay());
482   first = false;
483   EnsureGCRunner(delay);
484 }
485 
EnsureGCRunner(TimeDuration aDelay)486 void CCGCScheduler::EnsureGCRunner(TimeDuration aDelay) {
487   if (mGCRunner) {
488     return;
489   }
490 
491   // Wait at most the interslice GC delay before forcing a run.
492   mGCRunner = IdleTaskRunner::Create(
493       [this](TimeStamp aDeadline) { return GCRunnerFired(aDeadline); },
494       "CCGCScheduler::EnsureGCRunner", aDelay,
495       TimeDuration::FromMilliseconds(
496           StaticPrefs::javascript_options_gc_delay_interslice()),
497       mActiveIntersliceGCBudget, true, [this] { return mDidShutdown; },
498       [this](uint32_t) {
499         PROFILER_MARKER_UNTYPED("GC Interrupt", GCCC);
500         mInterruptRequested = true;
501       });
502 }
503 
504 // nsJSEnvironmentObserver observes the user-interaction-inactive notifications
505 // and triggers a shrinking a garbage collection if the user is still inactive
506 // after NS_SHRINKING_GC_DELAY ms later, if the appropriate pref is set.
UserIsInactive()507 void CCGCScheduler::UserIsInactive() {
508   mUserIsActive = false;
509   if (StaticPrefs::javascript_options_compact_on_user_inactive()) {
510     PokeShrinkingGC();
511   }
512 }
513 
UserIsActive()514 void CCGCScheduler::UserIsActive() {
515   mUserIsActive = true;
516   KillShrinkingGCTimer();
517   if (mIsCompactingOnUserInactive) {
518     mozilla::dom::AutoJSAPI jsapi;
519     jsapi.Init();
520     JS::AbortIncrementalGC(jsapi.cx());
521   }
522   MOZ_ASSERT(!mIsCompactingOnUserInactive);
523 }
524 
KillShrinkingGCTimer()525 void CCGCScheduler::KillShrinkingGCTimer() {
526   if (mShrinkingGCTimer) {
527     mShrinkingGCTimer->Cancel();
528     NS_RELEASE(mShrinkingGCTimer);
529   }
530 }
531 
KillFullGCTimer()532 void CCGCScheduler::KillFullGCTimer() {
533   if (mFullGCTimer) {
534     mFullGCTimer->Cancel();
535     NS_RELEASE(mFullGCTimer);
536   }
537 }
538 
KillGCRunner()539 void CCGCScheduler::KillGCRunner() {
540   // If we're in an incremental GC then killing the timer is only okay if
541   // we're shutting down.
542   MOZ_ASSERT(!(InIncrementalGC() && !mDidShutdown));
543   if (mGCRunner) {
544     mGCRunner->Cancel();
545     mGCRunner = nullptr;
546   }
547 }
548 
EnsureCCRunner(TimeDuration aDelay,TimeDuration aBudget)549 void CCGCScheduler::EnsureCCRunner(TimeDuration aDelay, TimeDuration aBudget) {
550   MOZ_ASSERT(!mDidShutdown);
551 
552   if (!mCCRunner) {
553     mCCRunner = IdleTaskRunner::Create(
554         CCRunnerFired, "EnsureCCRunner::CCRunnerFired", 0, aDelay, aBudget,
555         true, [this] { return mDidShutdown; });
556   } else {
557     mCCRunner->SetMinimumUsefulBudget(aBudget.ToMilliseconds());
558     nsIEventTarget* target = mozilla::GetCurrentEventTarget();
559     if (target) {
560       mCCRunner->SetTimer(aDelay, target);
561     }
562   }
563 }
564 
MaybePokeCC(TimeStamp aNow,uint32_t aSuspectedCCObjects)565 void CCGCScheduler::MaybePokeCC(TimeStamp aNow, uint32_t aSuspectedCCObjects) {
566   if (mCCRunner || mDidShutdown) {
567     return;
568   }
569 
570   CCReason reason = ShouldScheduleCC(aNow, aSuspectedCCObjects);
571   if (reason != CCReason::NO_REASON) {
572     // We can kill some objects before running forgetSkippable.
573     nsCycleCollector_dispatchDeferredDeletion();
574 
575     if (!mCCRunner) {
576       InitCCRunnerStateMachine(CCRunnerState::ReducePurple, reason);
577     }
578     EnsureCCRunner(kCCSkippableDelay, kForgetSkippableSliceDuration);
579   }
580 }
581 
KillCCRunner()582 void CCGCScheduler::KillCCRunner() {
583   UnblockCC();
584   DeactivateCCRunner();
585   if (mCCRunner) {
586     mCCRunner->Cancel();
587     mCCRunner = nullptr;
588   }
589 }
590 
KillAllTimersAndRunners()591 void CCGCScheduler::KillAllTimersAndRunners() {
592   KillShrinkingGCTimer();
593   KillCCRunner();
594   KillFullGCTimer();
595   KillGCRunner();
596 }
597 
ComputeCCSliceBudget(TimeStamp aDeadline,TimeStamp aCCBeginTime,TimeStamp aPrevSliceEndTime,TimeStamp aNow,bool * aPreferShorterSlices) const598 js::SliceBudget CCGCScheduler::ComputeCCSliceBudget(
599     TimeStamp aDeadline, TimeStamp aCCBeginTime, TimeStamp aPrevSliceEndTime,
600     TimeStamp aNow, bool* aPreferShorterSlices) const {
601   *aPreferShorterSlices =
602       aDeadline.IsNull() || (aDeadline - aNow) < kICCSliceBudget;
603 
604   TimeDuration baseBudget =
605       aDeadline.IsNull() ? kICCSliceBudget : aDeadline - aNow;
606 
607   if (aCCBeginTime.IsNull()) {
608     // If no CC is in progress, use the standard slice time.
609     return js::SliceBudget(js::TimeBudget(baseBudget));
610   }
611 
612   // Only run a limited slice if we're within the max running time.
613   MOZ_ASSERT(aNow >= aCCBeginTime);
614   TimeDuration runningTime = aNow - aCCBeginTime;
615   if (runningTime >= kMaxICCDuration) {
616     return js::SliceBudget::unlimited();
617   }
618 
619   const TimeDuration maxSlice =
620       TimeDuration::FromMilliseconds(MainThreadIdlePeriod::GetLongIdlePeriod());
621 
622   // Try to make up for a delay in running this slice.
623   MOZ_ASSERT(aNow >= aPrevSliceEndTime);
624   double sliceDelayMultiplier =
625       (aNow - aPrevSliceEndTime) / kICCIntersliceDelay;
626   TimeDuration delaySliceBudget =
627       std::min(baseBudget.MultDouble(sliceDelayMultiplier), maxSlice);
628 
629   // Increase slice budgets up to |maxSlice| as we approach
630   // half way through the ICC, to avoid large sync CCs.
631   double percentToHalfDone =
632       std::min(2.0 * (runningTime / kMaxICCDuration), 1.0);
633   TimeDuration laterSliceBudget = maxSlice.MultDouble(percentToHalfDone);
634 
635   // Note: We may have already overshot the deadline, in which case
636   // baseBudget will be negative and we will end up returning
637   // laterSliceBudget.
638   return js::SliceBudget(js::TimeBudget(
639       std::max({delaySliceBudget, laterSliceBudget, baseBudget})));
640 }
641 
ComputeInterSliceGCBudget(TimeStamp aDeadline,TimeStamp aNow)642 js::SliceBudget CCGCScheduler::ComputeInterSliceGCBudget(TimeStamp aDeadline,
643                                                          TimeStamp aNow) {
644   // We use longer budgets when the CC has been locked out but the CC has
645   // tried to run since that means we may have a significant amount of
646   // garbage to collect and it's better to GC in several longer slices than
647   // in a very long one.
648   TimeDuration budget =
649       aDeadline.IsNull() ? mActiveIntersliceGCBudget * 2 : aDeadline - aNow;
650   if (!mCCBlockStart) {
651     return CreateGCSliceBudget(budget, !aDeadline.IsNull(), false);
652   }
653 
654   TimeDuration blockedTime = aNow - mCCBlockStart;
655   TimeDuration maxSliceGCBudget = mActiveIntersliceGCBudget * 10;
656   double percentOfBlockedTime =
657       std::min(blockedTime / kMaxCCLockedoutTime, 1.0);
658   TimeDuration extendedBudget =
659       maxSliceGCBudget.MultDouble(percentOfBlockedTime);
660   if (budget >= extendedBudget) {
661     return CreateGCSliceBudget(budget, !aDeadline.IsNull(), false);
662   }
663 
664   // If the budget is being extended, do not allow it to be interrupted.
665   auto result = js::SliceBudget(js::TimeBudget(extendedBudget), nullptr);
666   result.idle = !aDeadline.IsNull();
667   result.extended = true;
668   return result;
669 }
670 
ShouldScheduleCC(TimeStamp aNow,uint32_t aSuspectedCCObjects) const671 CCReason CCGCScheduler::ShouldScheduleCC(TimeStamp aNow,
672                                          uint32_t aSuspectedCCObjects) const {
673   if (!mHasRunGC) {
674     return CCReason::NO_REASON;
675   }
676 
677   // Don't run consecutive CCs too often.
678   if (mCleanupsSinceLastGC && !mLastCCEndTime.IsNull()) {
679     if (aNow - mLastCCEndTime < kCCDelay) {
680       return CCReason::NO_REASON;
681     }
682   }
683 
684   // If GC hasn't run recently and forget skippable only cycle was run,
685   // don't start a new cycle too soon.
686   if ((mCleanupsSinceLastGC > kMajorForgetSkippableCalls) &&
687       !mLastForgetSkippableCycleEndTime.IsNull()) {
688     if (aNow - mLastForgetSkippableCycleEndTime <
689         kTimeBetweenForgetSkippableCycles) {
690       return CCReason::NO_REASON;
691     }
692   }
693 
694   return IsCCNeeded(aNow, aSuspectedCCObjects);
695 }
696 
AdvanceCCRunner(TimeStamp aDeadline,TimeStamp aNow,uint32_t aSuspectedCCObjects)697 CCRunnerStep CCGCScheduler::AdvanceCCRunner(TimeStamp aDeadline, TimeStamp aNow,
698                                             uint32_t aSuspectedCCObjects) {
699   struct StateDescriptor {
700     // When in this state, should we first check to see if we still have
701     // enough reason to CC?
702     bool mCanAbortCC;
703 
704     // If we do decide to abort the CC, should we still try to forget
705     // skippables one more time?
706     bool mTryFinalForgetSkippable;
707   };
708 
709   // The state descriptors for Inactive and Canceled will never actually be
710   // used. We will never call this function while Inactive, and Canceled is
711   // handled specially at the beginning.
712   constexpr StateDescriptor stateDescriptors[] = {
713       {false, false},  /* CCRunnerState::Inactive */
714       {false, false},  /* CCRunnerState::ReducePurple */
715       {true, true},    /* CCRunnerState::CleanupChildless */
716       {true, false},   /* CCRunnerState::CleanupContentUnbinder */
717       {false, false},  /* CCRunnerState::CleanupDeferred */
718       {false, false},  /* CCRunnerState::StartCycleCollection */
719       {false, false},  /* CCRunnerState::CycleCollecting */
720       {false, false}}; /* CCRunnerState::Canceled */
721   static_assert(
722       ArrayLength(stateDescriptors) == size_t(CCRunnerState::NumStates),
723       "need one state descriptor per state");
724   const StateDescriptor& desc = stateDescriptors[int(mCCRunnerState)];
725 
726   // Make sure we initialized the state machine.
727   MOZ_ASSERT(mCCRunnerState != CCRunnerState::Inactive);
728 
729   if (mDidShutdown) {
730     return {CCRunnerAction::StopRunning, Yield};
731   }
732 
733   if (mCCRunnerState == CCRunnerState::Canceled) {
734     // When we cancel a cycle, there may have been a final ForgetSkippable.
735     return {CCRunnerAction::StopRunning, Yield};
736   }
737 
738   if (InIncrementalGC()) {
739     if (mCCBlockStart.IsNull()) {
740       BlockCC(aNow);
741 
742       // If we have reached the CycleCollecting state, then ignore CC timer
743       // fires while incremental GC is running. (Running ICC during an IGC
744       // would cause us to synchronously finish the GC, which is bad.)
745       //
746       // If we have not yet started cycle collecting, then reset our state so
747       // that we run forgetSkippable often enough before CC. Because of reduced
748       // mCCDelay, forgetSkippable will be called just a few times.
749       //
750       // The kMaxCCLockedoutTime limit guarantees that we end up calling
751       // forgetSkippable and CycleCollectNow eventually.
752 
753       if (mCCRunnerState != CCRunnerState::CycleCollecting) {
754         mCCRunnerState = CCRunnerState::ReducePurple;
755         mCCRunnerEarlyFireCount = 0;
756         mCCDelay = kCCDelay / int64_t(3);
757       }
758       return {CCRunnerAction::None, Yield};
759     }
760 
761     if (GetCCBlockedTime(aNow) < kMaxCCLockedoutTime) {
762       return {CCRunnerAction::None, Yield};
763     }
764 
765     // Locked out for too long, so proceed and finish the incremental GC
766     // synchronously.
767   }
768 
769   // For states that aren't just continuations of previous states, check
770   // whether a CC is still needed (after doing various things to reduce the
771   // purple buffer).
772   if (desc.mCanAbortCC &&
773       IsCCNeeded(aNow, aSuspectedCCObjects) == CCReason::NO_REASON) {
774     // If we don't pass the threshold for wanting to cycle collect, stop now
775     // (after possibly doing a final ForgetSkippable).
776     mCCRunnerState = CCRunnerState::Canceled;
777     NoteForgetSkippableOnlyCycle(aNow);
778 
779     // Preserve the previous code's idea of when to check whether a
780     // ForgetSkippable should be fired.
781     if (desc.mTryFinalForgetSkippable &&
782         ShouldForgetSkippable(aSuspectedCCObjects)) {
783       // The Canceled state will make us StopRunning after this action is
784       // performed (see conditional at top of function).
785       return {CCRunnerAction::ForgetSkippable, Yield, KeepChildless};
786     }
787 
788     return {CCRunnerAction::StopRunning, Yield};
789   }
790 
791   switch (mCCRunnerState) {
792       // ReducePurple: a GC ran (or we otherwise decided to try CC'ing). Wait
793       // for some amount of time (kCCDelay, or less if incremental GC blocked
794       // this CC) while firing regular ForgetSkippable actions before continuing
795       // on.
796     case CCRunnerState::ReducePurple:
797       ++mCCRunnerEarlyFireCount;
798       if (IsLastEarlyCCTimer(mCCRunnerEarlyFireCount)) {
799         mCCRunnerState = CCRunnerState::CleanupChildless;
800       }
801 
802       if (ShouldForgetSkippable(aSuspectedCCObjects)) {
803         return {CCRunnerAction::ForgetSkippable, Yield, KeepChildless};
804       }
805 
806       if (aDeadline.IsNull()) {
807         return {CCRunnerAction::None, Yield};
808       }
809 
810       // If we're called during idle time, try to find some work to do by
811       // advancing to the next state, effectively bypassing some possible forget
812       // skippable calls.
813       mCCRunnerState = CCRunnerState::CleanupChildless;
814 
815       // Continue on to CleanupChildless, but only after checking IsCCNeeded
816       // again.
817       return {CCRunnerAction::None, Continue};
818 
819       // CleanupChildless: do a stronger ForgetSkippable that removes nodes with
820       // no children in the cycle collector graph. This state is split into 3
821       // parts; the other Cleanup* actions will happen within the same callback
822       // (unless the ForgetSkippable shrinks the purple buffer enough for the CC
823       // to be skipped entirely.)
824     case CCRunnerState::CleanupChildless:
825       mCCRunnerState = CCRunnerState::CleanupContentUnbinder;
826       return {CCRunnerAction::ForgetSkippable, Yield, RemoveChildless};
827 
828       // CleanupContentUnbinder: continuing cleanup, clear out the content
829       // unbinder.
830     case CCRunnerState::CleanupContentUnbinder:
831       if (aDeadline.IsNull()) {
832         // Non-idle (waiting) callbacks skip the rest of the cleanup, but still
833         // wait for another fire before the actual CC.
834         mCCRunnerState = CCRunnerState::StartCycleCollection;
835         return {CCRunnerAction::None, Yield};
836       }
837 
838       // Running in an idle callback.
839 
840       // The deadline passed, so go straight to CC in the next slice.
841       if (aNow >= aDeadline) {
842         mCCRunnerState = CCRunnerState::StartCycleCollection;
843         return {CCRunnerAction::None, Yield};
844       }
845 
846       mCCRunnerState = CCRunnerState::CleanupDeferred;
847       return {CCRunnerAction::CleanupContentUnbinder, Continue};
848 
849       // CleanupDeferred: continuing cleanup, do deferred deletion.
850     case CCRunnerState::CleanupDeferred:
851       MOZ_ASSERT(!aDeadline.IsNull(),
852                  "Should only be in CleanupDeferred state when idle");
853 
854       // Our efforts to avoid a CC have failed. Let the timer fire once more
855       // to trigger a CC.
856       mCCRunnerState = CCRunnerState::StartCycleCollection;
857       if (aNow >= aDeadline) {
858         // The deadline passed, go straight to CC in the next slice.
859         return {CCRunnerAction::None, Yield};
860       }
861 
862       return {CCRunnerAction::CleanupDeferred, Yield};
863 
864       // StartCycleCollection: start actually doing cycle collection slices.
865     case CCRunnerState::StartCycleCollection:
866       // We are in the final timer fire and still meet the conditions for
867       // triggering a CC. Let RunCycleCollectorSlice finish the current IGC if
868       // any, because that will allow us to include the GC time in the CC pause.
869       mCCRunnerState = CCRunnerState::CycleCollecting;
870       [[fallthrough]];
871 
872       // CycleCollecting: continue running slices until done.
873     case CCRunnerState::CycleCollecting: {
874       CCRunnerStep step{CCRunnerAction::CycleCollect, Yield};
875       step.mCCReason = mCCReason;
876       mCCReason = CCReason::SLICE;  // Set reason for following slices.
877       return step;
878     }
879 
880     default:
881       MOZ_CRASH("Unexpected CCRunner state");
882   };
883 }
884 
GetNextGCRunnerAction() const885 GCRunnerStep CCGCScheduler::GetNextGCRunnerAction() const {
886   MOZ_ASSERT(mMajorGCReason != JS::GCReason::NO_REASON);
887 
888   if (InIncrementalGC()) {
889     return {GCRunnerAction::GCSlice, mMajorGCReason};
890   }
891 
892   if (mReadyForMajorGC) {
893     return {GCRunnerAction::StartMajorGC, mMajorGCReason};
894   }
895 
896   return {GCRunnerAction::WaitToMajorGC, mMajorGCReason};
897 }
898 
ComputeForgetSkippableBudget(TimeStamp aStartTimeStamp,TimeStamp aDeadline)899 js::SliceBudget CCGCScheduler::ComputeForgetSkippableBudget(
900     TimeStamp aStartTimeStamp, TimeStamp aDeadline) {
901   if (mForgetSkippableFrequencyStartTime.IsNull()) {
902     mForgetSkippableFrequencyStartTime = aStartTimeStamp;
903   } else if (aStartTimeStamp - mForgetSkippableFrequencyStartTime >
904              kOneMinute) {
905     TimeStamp startPlusMinute = mForgetSkippableFrequencyStartTime + kOneMinute;
906 
907     // If we had forget skippables only at the beginning of the interval, we
908     // still want to use the whole time, minute or more, for frequency
909     // calculation. mLastForgetSkippableEndTime is needed if forget skippable
910     // takes enough time to push the interval to be over a minute.
911     TimeStamp endPoint = std::max(startPlusMinute, mLastForgetSkippableEndTime);
912 
913     // Duration in minutes.
914     double duration =
915         (endPoint - mForgetSkippableFrequencyStartTime).ToSeconds() / 60;
916     uint32_t frequencyPerMinute = uint32_t(mForgetSkippableCounter / duration);
917     Telemetry::Accumulate(Telemetry::FORGET_SKIPPABLE_FREQUENCY,
918                           frequencyPerMinute);
919     mForgetSkippableCounter = 0;
920     mForgetSkippableFrequencyStartTime = aStartTimeStamp;
921   }
922   ++mForgetSkippableCounter;
923 
924   TimeDuration budgetTime =
925       aDeadline ? (aDeadline - aStartTimeStamp) : kForgetSkippableSliceDuration;
926   return js::SliceBudget(budgetTime);
927 }
928 
929 }  // namespace mozilla
930