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 "js/SliceBudget.h"
6 #include "mozilla/ArrayUtils.h"
7 #include "mozilla/CycleCollectedJSContext.h"
8 #include "mozilla/IdleTaskRunner.h"
9 #include "mozilla/MainThreadIdlePeriod.h"
10 #include "mozilla/Telemetry.h"
11 #include "mozilla/TimeStamp.h"
12 #include "mozilla/ipc/IdleSchedulerChild.h"
13 #include "nsCycleCollector.h"
14 #include "nsJSEnvironment.h"
15 
16 namespace mozilla {
17 
18 static const TimeDuration kOneMinute = TimeDuration::FromSeconds(60.0f);
19 
20 // The amount of time we wait between a request to CC (after GC ran)
21 // and doing the actual CC.
22 static const TimeDuration kCCDelay = TimeDuration::FromSeconds(6);
23 
24 static const TimeDuration kCCSkippableDelay =
25     TimeDuration::FromMilliseconds(250);
26 
27 // In case the cycle collector isn't run at all, we don't want forget skippables
28 // to run too often. So limit the forget skippable cycle to start at earliest 2
29 // seconds after the end of the previous cycle.
30 static const TimeDuration kTimeBetweenForgetSkippableCycles =
31     TimeDuration::FromSeconds(2);
32 
33 // ForgetSkippable is usually fast, so we can use small budgets.
34 // This isn't a real budget but a hint to IdleTaskRunner whether there
35 // is enough time to call ForgetSkippable.
36 static const TimeDuration kForgetSkippableSliceDuration =
37     TimeDuration::FromMilliseconds(2);
38 
39 // Maximum amount of time that should elapse between incremental CC slices
40 static const TimeDuration kICCIntersliceDelay =
41     TimeDuration::FromMilliseconds(64);
42 
43 // Time budget for an incremental CC slice when using timer to run it.
44 static const TimeDuration kICCSliceBudget = TimeDuration::FromMilliseconds(3);
45 // Minimum budget for an incremental CC slice when using idle time to run it.
46 static const TimeDuration kIdleICCSliceBudget =
47     TimeDuration::FromMilliseconds(2);
48 
49 // Maximum total duration for an ICC
50 static const TimeDuration kMaxICCDuration = TimeDuration::FromSeconds(2);
51 
52 // Force a CC after this long if there's more than NS_CC_FORCED_PURPLE_LIMIT
53 // objects in the purple buffer.
54 static const TimeDuration kCCForced = kOneMinute * 2;
55 static const uint32_t kCCForcedPurpleLimit = 10;
56 
57 // Don't allow an incremental GC to lock out the CC for too long.
58 static const TimeDuration kMaxCCLockedoutTime = TimeDuration::FromSeconds(30);
59 
60 // Trigger a CC if the purple buffer exceeds this size when we check it.
61 static const uint32_t kCCPurpleLimit = 200;
62 
63 // How many cycle collected nodes to traverse between time checks.
64 static const int64_t kNumCCNodesBetweenTimeChecks = 1000;
65 
66 // Actions performed by the GCRunner state machine.
67 enum class GCRunnerAction {
68   WaitToMajorGC,  // We want to start a new major GC
69   StartMajorGC,   // The parent says we may begin our major GC
70   GCSlice,        // Run a single slice of a major GC
71   None
72 };
73 
74 struct GCRunnerStep {
75   GCRunnerAction mAction;
76   JS::GCReason mReason;
77 };
78 
79 enum class CCRunnerAction {
80   None,
81   ForgetSkippable,
82   CleanupContentUnbinder,
83   CleanupDeferred,
84   CycleCollect,
85   StopRunning
86 };
87 
88 enum CCRunnerYield { Continue, Yield };
89 
90 enum CCRunnerForgetSkippableRemoveChildless {
91   KeepChildless = false,
92   RemoveChildless = true
93 };
94 
95 struct CCRunnerStep {
96   // The action to scheduler is instructing the caller to perform.
97   CCRunnerAction mAction;
98 
99   // Whether to stop processing actions for this invocation of the timer
100   // callback.
101   CCRunnerYield mYield;
102 
103   // If the action is ForgetSkippable, then whether to remove childless nodes
104   // or not. (ForgetSkippable is the only action requiring a parameter; if
105   // that changes, this will become a union.)
106   CCRunnerForgetSkippableRemoveChildless mRemoveChildless;
107 };
108 
109 class CCGCScheduler {
110  public:
111   static bool CCRunnerFired(TimeStamp aDeadline);
112 
113   // Parameter setting
114 
SetActiveIntersliceGCBudget(TimeDuration aDuration)115   void SetActiveIntersliceGCBudget(TimeDuration aDuration) {
116     mActiveIntersliceGCBudget = aDuration;
117   }
118 
119   // State retrieval
120 
GetCCBlockedTime(TimeStamp aNow)121   TimeDuration GetCCBlockedTime(TimeStamp aNow) const {
122     MOZ_ASSERT(mInIncrementalGC);
123     MOZ_ASSERT(!mCCBlockStart.IsNull());
124     return aNow - mCCBlockStart;
125   }
126 
InIncrementalGC()127   bool InIncrementalGC() const { return mInIncrementalGC; }
128 
GetLastCCEndTime()129   TimeStamp GetLastCCEndTime() const { return mLastCCEndTime; }
130 
131   bool IsEarlyForgetSkippable(uint32_t aN = kMajorForgetSkippableCalls) const {
132     return mCleanupsSinceLastGC < aN;
133   }
134 
NeedsFullGC()135   bool NeedsFullGC() const { return mNeedsFullGC; }
136 
137   // Requests
138   void PokeGC(JS::GCReason aReason, JSObject* aObj, uint32_t aDelay = 0);
139   void PokeShrinkingGC();
140   void PokeFullGC();
141   void MaybePokeCC(TimeStamp aNow, uint32_t aSuspectedCCObjects);
142 
143   void UserIsInactive();
144   void UserIsActive();
145 
146   void KillShrinkingGCTimer();
147   void KillFullGCTimer();
148   void KillGCRunner();
149   void KillCCRunner();
150   void KillAllTimersAndRunners();
151 
152   /*
153    * aDelay is the delay before the first time the idle task runner runs.
154    * Then it runs every
155    * StaticPrefs::javascript_options_gc_delay_interslice()
156    */
157   void EnsureGCRunner(uint32_t aDelay);
158 
159   void EnsureCCRunner(TimeDuration aDelay, TimeDuration aBudget);
160 
161   // State modification
162 
163   void SetNeedsFullGC(bool aNeedGC = true) { mNeedsFullGC = aNeedGC; }
164 
SetWantMajorGC(JS::GCReason aReason)165   void SetWantMajorGC(JS::GCReason aReason) {
166     mMajorGCReason = aReason;
167 
168     // Force full GCs when called from reftests so that we collect dead zones
169     // that have not been scheduled for collection.
170     if (aReason == JS::GCReason::DOM_WINDOW_UTILS) {
171       SetNeedsFullGC();
172     }
173   }
174   // Ensure that the current runner does a cycle collection, and trigger a GC
175   // after it finishes.
EnsureCCThenGC()176   void EnsureCCThenGC() {
177     MOZ_ASSERT(mCCRunnerState != CCRunnerState::Inactive);
178     mNeedsFullCC = true;
179     mNeedsGCAfterCC = true;
180   }
181 
182   // Returns false if we started and finished a major GC while waiting for a
183   // response.
NoteReadyForMajorGC()184   [[nodiscard]] bool NoteReadyForMajorGC() {
185     if (mMajorGCReason == JS::GCReason::NO_REASON) {
186       return false;
187     }
188     mReadyForMajorGC = true;
189     return true;
190   }
191 
NoteGCBegin()192   void NoteGCBegin() {
193     // Treat all GC as incremental here; non-incremental GC will just appear to
194     // be one slice.
195     mInIncrementalGC = true;
196     mReadyForMajorGC = false;
197   }
198 
NoteGCEnd()199   void NoteGCEnd() {
200     mMajorGCReason = JS::GCReason::NO_REASON;
201 
202     mInIncrementalGC = false;
203     mCCBlockStart = TimeStamp();
204     mInIncrementalGC = false;
205     mReadyForMajorGC = false;
206     mNeedsFullCC = true;
207     mHasRunGC = true;
208     mIsCompactingOnUserInactive = false;
209 
210     mCleanupsSinceLastGC = 0;
211     mCCollectedWaitingForGC = 0;
212     mCCollectedZonesWaitingForGC = 0;
213     mLikelyShortLivingObjectsNeedingGC = 0;
214   }
215 
NoteGCSliceEnd(TimeDuration aSliceDuration)216   void NoteGCSliceEnd(TimeDuration aSliceDuration) {
217     if (mMajorGCReason == JS::GCReason::NO_REASON) {
218       // Internally-triggered GCs do not wait for the parent's permission to
219       // proceed. This flag won't be checked during an incremental GC anyway,
220       // but it better reflects reality.
221       mReadyForMajorGC = true;
222     }
223 
224     // Subsequent slices should be INTER_SLICE_GC unless they are triggered by
225     // something else that provides its own reason.
226     mMajorGCReason = JS::GCReason::INTER_SLICE_GC;
227 
228     mGCUnnotifiedTotalTime += aSliceDuration;
229   }
230 
231   void FullGCTimerFired(nsITimer* aTimer);
232   void ShrinkingGCTimerFired(nsITimer* aTimer);
233   bool GCRunnerFired(TimeStamp aDeadline);
234 
235   using MayGCPromise =
236       MozPromise<bool /* aIgnored */, mozilla::ipc::ResponseRejectReason, true>;
237 
238   // Returns null if we shouldn't GC now (eg a GC is already running).
239   static RefPtr<MayGCPromise> MayGCNow(JS::GCReason reason);
240 
241   // Check all of the various collector timers/runners and see if they are
242   // waiting to fire. This does not check the Full GC Timer, as that's a
243   // more expensive collection we run on a long timer.
244   void RunNextCollectorTimer(JS::GCReason aReason,
245                              mozilla::TimeStamp aDeadline);
246 
247   // When we decide to do a cycle collection but we're in the middle of an
248   // incremental GC, the CC is "locked out" until the GC completes -- unless
249   // the wait is too long, and we decide to finish the incremental GC early.
BlockCC(TimeStamp aNow)250   void BlockCC(TimeStamp aNow) {
251     MOZ_ASSERT(mInIncrementalGC);
252     MOZ_ASSERT(mCCBlockStart.IsNull());
253     mCCBlockStart = aNow;
254   }
255 
UnblockCC()256   void UnblockCC() { mCCBlockStart = TimeStamp(); }
257 
258   // Returns the number of purple buffer items that were processed and removed.
NoteForgetSkippableComplete(TimeStamp aNow,uint32_t aSuspectedBeforeForgetSkippable,uint32_t aSuspectedCCObjects)259   uint32_t NoteForgetSkippableComplete(TimeStamp aNow,
260                                        uint32_t aSuspectedBeforeForgetSkippable,
261                                        uint32_t aSuspectedCCObjects) {
262     mLastForgetSkippableEndTime = aNow;
263     mPreviousSuspectedCount = aSuspectedCCObjects;
264     mCleanupsSinceLastGC++;
265     return aSuspectedBeforeForgetSkippable - aSuspectedCCObjects;
266   }
267 
268   // After collecting cycles, record the results that are used in scheduling
269   // decisions.
NoteCycleCollected(const CycleCollectorResults & aResults)270   void NoteCycleCollected(const CycleCollectorResults& aResults) {
271     mCCollectedWaitingForGC += aResults.mFreedGCed;
272     mCCollectedZonesWaitingForGC += aResults.mFreedJSZones;
273   }
274 
275   // This is invoked when the whole process of collection is done -- i.e., CC
276   // preparation (eg ForgetSkippables), the CC itself, and the optional
277   // followup GC. There really ought to be a separate name for the overall CC
278   // as opposed to the actual cycle collection portion.
NoteCCEnd(TimeStamp aWhen)279   void NoteCCEnd(TimeStamp aWhen) {
280     mLastCCEndTime = aWhen;
281     mNeedsFullCC = false;
282 
283     // The GC for this CC has already been requested.
284     mNeedsGCAfterCC = false;
285   }
286 
287   // The CC was abandoned without running a slice, so we only did forget
288   // skippables. Prevent running another cycle soon.
NoteForgetSkippableOnlyCycle(TimeStamp aNow)289   void NoteForgetSkippableOnlyCycle(TimeStamp aNow) {
290     mLastForgetSkippableCycleEndTime = aNow;
291   }
292 
Shutdown()293   void Shutdown() {
294     mDidShutdown = true;
295     KillAllTimersAndRunners();
296   }
297 
298   // Scheduling
299 
300   // Return a budget along with a boolean saying whether to prefer to run short
301   // slices and stop rather than continuing to the next phase of cycle
302   // collection.
303   js::SliceBudget ComputeCCSliceBudget(TimeStamp aDeadline,
304                                        TimeStamp aCCBeginTime,
305                                        TimeStamp aPrevSliceEndTime,
306                                        TimeStamp aNow,
307                                        bool* aPreferShorterSlices) const;
308 
309   TimeDuration ComputeInterSliceGCBudget(TimeStamp aDeadline,
310                                          TimeStamp aNow) const;
311 
ShouldForgetSkippable(uint32_t aSuspectedCCObjects)312   bool ShouldForgetSkippable(uint32_t aSuspectedCCObjects) const {
313     // Only do a forget skippable if there are more than a few new objects
314     // or we're doing the initial forget skippables.
315     return ((mPreviousSuspectedCount + 100) <= aSuspectedCCObjects) ||
316            mCleanupsSinceLastGC < kMajorForgetSkippableCalls;
317   }
318 
319   // There is reason to suspect that there may be a significant amount of
320   // garbage to cycle collect: either we just finished a GC, or the purple
321   // buffer is getting really big, or it's getting somewhat big and it has been
322   // too long since the last CC.
IsCCNeeded(TimeStamp aNow,uint32_t aSuspectedCCObjects)323   bool IsCCNeeded(TimeStamp aNow, uint32_t aSuspectedCCObjects) const {
324     if (mNeedsFullCC) {
325       return true;
326     }
327     return aSuspectedCCObjects > kCCPurpleLimit ||
328            (aSuspectedCCObjects > kCCForcedPurpleLimit && mLastCCEndTime &&
329             aNow - mLastCCEndTime > kCCForced);
330   }
331 
332   bool ShouldScheduleCC(TimeStamp aNow, uint32_t aSuspectedCCObjects) const;
333 
334   // If we collected a substantial amount of cycles, poke the GC since more
335   // objects might be unreachable now.
NeedsGCAfterCC()336   bool NeedsGCAfterCC() const {
337     return mCCollectedWaitingForGC > 250 || mCCollectedZonesWaitingForGC > 0 ||
338            mLikelyShortLivingObjectsNeedingGC > 2500 || mNeedsGCAfterCC;
339   }
340 
IsLastEarlyCCTimer(int32_t aCurrentFireCount)341   bool IsLastEarlyCCTimer(int32_t aCurrentFireCount) const {
342     int32_t numEarlyTimerFires =
343         std::max(int32_t(mCCDelay / kCCSkippableDelay) - 2, 1);
344 
345     return aCurrentFireCount >= numEarlyTimerFires;
346   }
347 
348   enum class CCRunnerState {
349     Inactive,
350     ReducePurple,
351     CleanupChildless,
352     CleanupContentUnbinder,
353     CleanupDeferred,
354     StartCycleCollection,
355     CycleCollecting,
356     Canceled,
357     NumStates
358   };
359 
InitCCRunnerStateMachine(CCRunnerState initialState)360   void InitCCRunnerStateMachine(CCRunnerState initialState) {
361     if (mCCRunner) {
362       return;
363     }
364 
365     // The state machine should always have been deactivated after the previous
366     // collection, however far that collection may have gone.
367     MOZ_ASSERT(mCCRunnerState == CCRunnerState::Inactive,
368                "DeactivateCCRunner should have been called");
369     mCCRunnerState = initialState;
370 
371     // Currently, there are only two entry points to the non-Inactive part of
372     // the state machine.
373     if (initialState == CCRunnerState::ReducePurple) {
374       mCCDelay = kCCDelay;
375       mCCRunnerEarlyFireCount = 0;
376     } else if (initialState == CCRunnerState::CycleCollecting) {
377       // Nothing needed.
378     } else {
379       MOZ_CRASH("Invalid initial state");
380     }
381   }
382 
DeactivateCCRunner()383   void DeactivateCCRunner() { mCCRunnerState = CCRunnerState::Inactive; }
384 
385   GCRunnerStep GetNextGCRunnerAction(TimeStamp aDeadline);
386 
387   CCRunnerStep AdvanceCCRunner(TimeStamp aDeadline, TimeStamp aNow,
388                                uint32_t aSuspectedCCObjects);
389 
390   // aStartTimeStamp : when the ForgetSkippable timer fired. This may be some
391   // time ago, if an incremental GC needed to be finished.
392   js::SliceBudget ComputeForgetSkippableBudget(TimeStamp aStartTimeStamp,
393                                                TimeStamp aDeadline);
394 
395  private:
396   // State
397 
398   // An incremental GC is in progress, which blocks the CC from running for its
399   // duration (or until it goes too long and is finished synchronously.)
400   bool mInIncrementalGC = false;
401 
402   // The parent process is ready for us to do a major GC.
403   bool mReadyForMajorGC = false;
404 
405   // When the CC started actually waiting for the GC to finish. This will be
406   // set to non-null at a later time than mCCLockedOut.
407   TimeStamp mCCBlockStart;
408 
409   bool mDidShutdown = false;
410 
411   TimeStamp mLastForgetSkippableEndTime;
412   uint32_t mForgetSkippableCounter = 0;
413   TimeStamp mForgetSkippableFrequencyStartTime;
414   TimeStamp mLastCCEndTime;
415   TimeStamp mLastForgetSkippableCycleEndTime;
416 
417   CCRunnerState mCCRunnerState = CCRunnerState::Inactive;
418   int32_t mCCRunnerEarlyFireCount = 0;
419   TimeDuration mCCDelay = kCCDelay;
420 
421   // Prevent the very first CC from running before we have GC'd and set the
422   // gray bits.
423   bool mHasRunGC = false;
424 
425   bool mNeedsFullCC = false;
426   bool mNeedsFullGC = true;
427   bool mNeedsGCAfterCC = false;
428   uint32_t mPreviousSuspectedCount = 0;
429 
430   uint32_t mCleanupsSinceLastGC = UINT32_MAX;
431 
432   TimeDuration mGCUnnotifiedTotalTime;
433 
434   RefPtr<IdleTaskRunner> mGCRunner;
435   RefPtr<IdleTaskRunner> mCCRunner;
436   nsITimer* mShrinkingGCTimer = nullptr;
437   nsITimer* mFullGCTimer = nullptr;
438 
439   JS::GCReason mMajorGCReason = JS::GCReason::NO_REASON;
440 
441   bool mIsCompactingOnUserInactive = false;
442   bool mUserIsActive = true;
443 
444  public:
445   uint32_t mCCollectedWaitingForGC = 0;
446   uint32_t mCCollectedZonesWaitingForGC = 0;
447   uint32_t mLikelyShortLivingObjectsNeedingGC = 0;
448 
449   // Configuration parameters
450 
451   TimeDuration mActiveIntersliceGCBudget = TimeDuration::FromMilliseconds(5);
452 };
453 
454 }  // namespace mozilla
455