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