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