Bug 1890277: part 2) Add `require-trusted-types-for` directive to CSP parser, guarded...
[gecko.git] / dom / base / CCGCScheduler.cpp
blob1374d6e6fb5695f92d18932acf054e438ca50df7
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"
7 #include "js/GCAPI.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
17 namespace mozilla {
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
45 * * PokeGC()
46 * * PokeFullGC()
47 * * PokeShrinkingGC()
48 * * memory-pressure GCs (via a listener in nsJSEnvironment.cpp).
50 * PokeGC
51 * ------
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.
67 * PokeFullGC
68 * ----------
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.
82 * PokeShrinkingGC
83 * ---------------
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
109 * * Exit
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
128 * various kinds.
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 "
138 "slices.";
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) {
181 uint32_t none = 0;
182 if (aIsStart) {
183 ETW::OutputMarkerSchema(aContext, CCIntervalMarker{}, aReason,
184 mozilla::TimeDuration{}, aSuspectedAtCCStart,
185 none, false, false, false,
186 aForgetSkippableBeforeCC, none, none, none, none,
187 none, aRemovedPurples);
188 } else {
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) {
205 if (aIsStart) {
206 aWriter.StringProperty("mReason", aReason);
207 aWriter.IntProperty("mSuspected", aSuspectedAtCCStart);
208 aWriter.IntProperty("mForgetSkippable", aForgetSkippableBeforeCC);
209 aWriter.IntProperty("mRemovedPurples", aRemovedPurples);
210 } else {
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
228 namespace mozilla {
230 void CCGCScheduler::NoteGCBegin(JS::GCReason aReason) {
231 // Treat all GC as incremental here; non-incremental GC will just appear to
232 // be one slice.
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();
240 if (child) {
241 child->StartedGC();
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;
263 mHasRunGC = true;
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();
273 if (child) {
274 child->DoneGC();
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;
301 if (!startedIdle) {
302 nonIdleDuration = sliceDuration;
303 } else {
304 if (*mTriggeredGCDeadline < aEnd) {
305 // Overran the idle deadline.
306 nonIdleDuration = aEnd - *mTriggeredGCDeadline;
310 PerfStats::RecordMeasurement(PerfStats::Metric::NonIdleMajorGC,
311 nonIdleDuration);
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;
320 uint32_t percent =
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;
331 PROFILER_MARKER(
332 "CC", GCCC, MarkerOptions(MarkerTiming::IntervalStart(aWhen)),
333 CCIntervalMarker,
334 /* aIsStart */ true,
335 ProfilerString8View::WrapNullTerminatedString(CCReasonToString(aReason)),
336 aNumForgetSkippables, aSuspected, aRemovedPurples, ignoredResults,
337 TimeDuration());
339 mIsCollectingCycles = true;
342 void CCGCScheduler::NoteCCEnd(const CycleCollectorResults& aResults,
343 TimeStamp aWhen,
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:
372 KillGCRunner();
373 return false;
375 case GCRunnerAction::MinorGC:
376 JS::MaybeRunNurseryCollection(CycleCollectedJSRuntime::Get()->Runtime(),
377 step.mReason);
378 NoteMinorGCEnd();
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);
385 if (!mbPromise) {
386 // We can GC now.
387 break;
390 mHaveAskedParent = true;
391 KillGCRunner();
392 mbPromise->Then(
393 GetMainThreadSerialEventTarget(), __func__,
394 [this](bool aMayGC) {
395 mHaveAskedParent = false;
396 if (aMayGC) {
397 if (!NoteReadyForMajorGC()) {
398 // Another GC started and maybe completed while waiting.
399 return;
401 // Recreate the GC runner with a 0 delay. The new runner will
402 // continue in idle time.
403 KillGCRunner();
404 EnsureGCRunner(0);
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.
408 KillGCRunner();
409 NoteWontGC();
412 [this](mozilla::ipc::ResponseRejectReason r) {
413 mHaveAskedParent = false;
414 if (!InIncrementalGC()) {
415 KillGCRunner();
416 NoteWontGC();
420 return true;
423 case GCRunnerAction::StartMajorGC:
424 case GCRunnerAction::GCSlice:
425 break;
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;
442 do_gc = true;
443 } else {
444 // Poke again to restart the timer.
445 PokeShrinkingGC();
449 if (!do_gc) {
450 using mozilla::ipc::IdleSchedulerChild;
451 IdleSchedulerChild* child =
452 IdleSchedulerChild::GetMainThreadIdleScheduler();
453 if (child) {
454 child->DoneGC();
456 NoteWontGC();
457 KillGCRunner();
458 return true;
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.
486 switch (reason) {
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();
495 if (child) {
496 return child->MayGCNow();
499 // The parent process doesn't ask IdleSchedulerParent if it can GC.
500 break;
502 default:
503 break;
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__);
512 return p;
515 void CCGCScheduler::RunNextCollectorTimer(JS::GCReason aReason,
516 mozilla::TimeStamp aDeadline) {
517 if (mDidShutdown) {
518 return;
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;
526 if (mGCRunner) {
527 SetWantMajorGC(aReason);
528 runner = mGCRunner;
529 } else if (mCCRunner) {
530 runner = mCCRunner;
533 if (runner) {
534 runner->SetIdleDeadline(aDeadline);
535 runner->Run();
539 void CCGCScheduler::PokeShrinkingGC() {
540 if (mShrinkingGCTimer || mDidShutdown) {
541 return;
544 NS_NewTimerWithFuncCallback(
545 &mShrinkingGCTimer,
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);
555 } else {
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(
567 &mFullGCTimer,
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.
574 s->SetNeedsFullGC();
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);
590 if (mDidShutdown) {
591 return;
594 // If a post-CC GC was pending, then we'll make sure one is happening.
595 mNeedsGCAfterCC = false;
597 if (aObj) {
598 JS::Zone* zone = JS::GetObjectZone(aObj);
599 CycleCollectedJSRuntime::Get()->AddZoneWaitingForGC(zone);
600 } else if (aReason != JS::GCReason::CC_FINISHED) {
601 SetNeedsFullGC();
604 if (mGCRunner || mHaveAskedParent) {
605 // There's already a GC runner, or there will be, so just return.
606 return;
609 SetWantMajorGC(aReason);
611 if (mCCRunner) {
612 // Make sure CC is called regardless of the size of the purple buffer, and
613 // GC after it.
614 EnsureCCThenGC(CCReason::GC_WAITING);
615 return;
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;
621 TimeDuration delay =
622 aDelay ? aDelay
623 : TimeDuration::FromMilliseconds(
624 first ? StaticPrefs::javascript_options_gc_delay_first()
625 : StaticPrefs::javascript_options_gc_delay());
626 first = false;
627 EnsureGCRunner(delay);
630 void CCGCScheduler::PokeMinorGC(JS::GCReason aReason) {
631 MOZ_ASSERT(aReason != JS::GCReason::NO_REASON);
633 if (mDidShutdown) {
634 return;
637 SetWantEagerMinorGC(aReason);
639 if (mGCRunner || mHaveAskedParent || mCCRunner) {
640 // There's already a runner, or there will be, so just return.
641 return;
644 // Immediately start looking for idle time to run the minor GC.
645 EnsureGCRunner(0);
648 void CCGCScheduler::EnsureOrResetGCRunner() {
649 if (!mGCRunner) {
650 EnsureGCRunner(0);
651 return;
654 mGCRunner->ResetTimer(TimeDuration::FromMilliseconds(
655 StaticPrefs::javascript_options_gc_delay_interslice()));
658 void CCGCScheduler::EnsureGCRunner(TimeDuration aDelay) {
659 if (mGCRunner) {
660 return;
663 TimeDuration minimumBudget = TimeDuration::FromMilliseconds(
664 std::max(nsRefreshDriver::HighRateMultiplier() *
665 mActiveIntersliceGCBudget.ToMilliseconds(),
666 1.0));
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; },
675 [this](uint32_t) {
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()) {
687 PokeShrinkingGC();
691 void CCGCScheduler::UserIsActive() {
692 mUserIsActive = true;
693 KillShrinkingGCTimer();
694 if (mIsCompactingOnUserInactive) {
695 mozilla::dom::AutoJSAPI jsapi;
696 jsapi.Init();
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() {
710 if (mFullGCTimer) {
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));
720 if (mGCRunner) {
721 mGCRunner->Cancel();
722 mGCRunner = nullptr;
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));
732 if (!mCCRunner) {
733 mCCRunner = IdleTaskRunner::Create(
734 CCRunnerFired, "EnsureCCRunner::CCRunnerFired", 0, aDelay,
735 minimumBudget, true, [this] { return mDidShutdown; });
736 } else {
737 mCCRunner->SetMinimumUsefulBudget(minimumBudget.ToMilliseconds());
738 nsIEventTarget* target = mozilla::GetCurrentSerialEventTarget();
739 if (target) {
740 mCCRunner->SetTimer(aDelay, target);
745 void CCGCScheduler::MaybePokeCC(TimeStamp aNow, uint32_t aSuspectedCCObjects) {
746 if (mCCRunner || mDidShutdown) {
747 return;
750 CCReason reason = ShouldScheduleCC(aNow, aSuspectedCCObjects);
751 if (reason != CCReason::NO_REASON) {
752 // We can kill some objects before running forgetSkippable.
753 nsCycleCollector_dispatchDeferredDeletion();
755 if (!mCCRunner) {
756 InitCCRunnerStateMachine(CCRunnerState::ReducePurple, reason);
758 EnsureCCRunner(kCCSkippableDelay, kForgetSkippableSliceDuration);
762 void CCGCScheduler::KillCCRunner() {
763 UnblockCC();
764 DeactivateCCRunner();
765 if (mCCRunner) {
766 mCCRunner->Cancel();
767 mCCRunner = nullptr;
771 void CCGCScheduler::KillAllTimersAndRunners() {
772 KillShrinkingGCTimer();
773 KillCCRunner();
774 KillFullGCTimer();
775 KillGCRunner();
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
817 // laterSliceBudget.
818 return js::SliceBudget(js::TimeBudget(
819 std::max({delaySliceBudget, laterSliceBudget, baseBudget})));
822 js::SliceBudget CCGCScheduler::ComputeInterSliceGCBudget(TimeStamp aDeadline,
823 TimeStamp aNow) {
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;
848 return result;
851 CCReason CCGCScheduler::ShouldScheduleCC(TimeStamp aNow,
852 uint32_t aSuspectedCCObjects) const {
853 if (!mHasRunGC) {
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?
882 bool mCanAbortCC;
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 */
901 static_assert(
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);
909 if (mDidShutdown) {
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()) {
920 BlockCC(aNow);
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
946 // synchronously.
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
951 // purple buffer).
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
979 // on.
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
996 // skippable calls.
997 mCCRunnerState = CCRunnerState::CleanupChildless;
999 // Continue on to CleanupChildless, but only after checking IsCCNeeded
1000 // again.
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
1013 // unbinder.
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
1039 // to trigger a CC.
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;
1054 [[fallthrough]];
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.
1061 return step;
1064 default:
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,
1079 mMajorGCReason};
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 >
1103 kOneMinute) {
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.
1113 double duration =
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