TimeoutManager.cpp (51928B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "TimeoutManager.h" 8 9 #include "TimeoutExecutor.h" 10 #include "mozilla/Logging.h" 11 #include "mozilla/MediaManager.h" 12 #include "mozilla/ProfilerMarkers.h" 13 #include "mozilla/ScopeExit.h" 14 #include "mozilla/StaticPrefs_dom.h" 15 #include "mozilla/StaticPrefs_privacy.h" 16 #include "mozilla/ThrottledEventQueue.h" 17 #include "mozilla/TimeStamp.h" 18 #include "mozilla/dom/ContentChild.h" 19 #include "mozilla/dom/DocGroup.h" 20 #include "mozilla/dom/Document.h" 21 #include "mozilla/dom/PopupBlocker.h" 22 #include "mozilla/dom/TimeoutHandler.h" 23 #include "mozilla/dom/WebTaskScheduler.h" 24 #include "mozilla/dom/WorkerScope.h" 25 #include "mozilla/net/WebSocketEventService.h" 26 #include "nsGlobalWindowInner.h" 27 #include "nsIGlobalObject.h" 28 #include "nsINamed.h" 29 30 using namespace mozilla; 31 using namespace mozilla::dom; 32 33 LazyLogModule gTimeoutLog("Timeout"); 34 35 TimeoutBudgetManager TimeoutManager::sBudgetManager{}; 36 37 // static 38 const uint32_t TimeoutManager::InvalidFiringId = 0; 39 40 namespace { 41 static int32_t gRunningTimeoutDepth = 0; 42 43 double GetRegenerationFactor(bool aIsBackground) { 44 // Lookup function for "dom.timeout.{background, 45 // foreground}_budget_regeneration_rate". 46 47 // Returns the rate of regeneration of the execution budget as a 48 // fraction. If the value is 1.0, the amount of time regenerated is 49 // equal to time passed. At this rate we regenerate 1ms/ms. If it is 50 // 0.01 the amount regenerated is 1% of time passed. At this rate we 51 // regenerate 1ms/100ms, etc. 52 double denominator = std::max( 53 aIsBackground 54 ? StaticPrefs::dom_timeout_background_budget_regeneration_rate() 55 : StaticPrefs::dom_timeout_foreground_budget_regeneration_rate(), 56 1); 57 return 1.0 / denominator; 58 } 59 60 TimeDuration GetMaxBudget(bool aIsBackground) { 61 // Lookup function for "dom.timeout.{background, 62 // foreground}_throttling_max_budget". 63 64 // Returns how high a budget can be regenerated before being 65 // clamped. If this value is less or equal to zero, 66 // TimeDuration::Forever() is implied. 67 int32_t maxBudget = 68 aIsBackground 69 ? StaticPrefs::dom_timeout_background_throttling_max_budget() 70 : StaticPrefs::dom_timeout_foreground_throttling_max_budget(); 71 return maxBudget > 0 ? TimeDuration::FromMilliseconds(maxBudget) 72 : TimeDuration::Forever(); 73 } 74 75 TimeDuration GetMinBudget(bool aIsBackground) { 76 // The minimum budget is computed by looking up the maximum allowed 77 // delay and computing how long time it would take to regenerate 78 // that budget using the regeneration factor. This number is 79 // expected to be negative. 80 return TimeDuration::FromMilliseconds( 81 -StaticPrefs::dom_timeout_budget_throttling_max_delay() / 82 std::max( 83 aIsBackground 84 ? StaticPrefs::dom_timeout_background_budget_regeneration_rate() 85 : StaticPrefs::dom_timeout_foreground_budget_regeneration_rate(), 86 1)); 87 } 88 } // namespace 89 90 // 91 nsGlobalWindowInner* TimeoutManager::GetInnerWindow() const { 92 return nsGlobalWindowInner::Cast(mGlobalObject.GetAsInnerWindow()); 93 } 94 95 bool TimeoutManager::IsBackground() const { 96 return !IsActive() && mGlobalObject.IsBackgroundInternal(); 97 } 98 99 bool TimeoutManager::IsActive() const { 100 // A window/worker is considered active if: 101 // * It is a chrome window/worker 102 // * It is playing audio 103 // 104 // Note that a window/worker can be considered active if it is either in the 105 // foreground or in the background. 106 107 nsGlobalWindowInner* window = GetInnerWindow(); 108 if (window && window->IsChromeWindow()) { 109 return true; 110 } 111 112 if (mIsChromeWorker) { 113 return true; 114 } 115 116 // Check if we're playing audio 117 if (mGlobalObject.IsPlayingAudio()) { 118 return true; 119 } 120 121 return false; 122 } 123 124 void TimeoutManager::SetLoading(bool value) { 125 // When moving from loading to non-loading, we may need to 126 // reschedule any existing timeouts from the idle timeout queue 127 // to the normal queue. 128 MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("%p: SetLoading(%d)", this, value)); 129 if (mIsLoading && !value) { 130 MoveIdleToActive(); 131 } 132 // We don't immediately move existing timeouts to the idle queue if we 133 // move to loading. When they would have fired, we'll see we're loading 134 // and move them then. 135 mIsLoading = value; 136 } 137 138 void TimeoutManager::MoveIdleToActive() { 139 uint32_t num = 0; 140 TimeStamp when; 141 TimeStamp now; 142 // Ensure we maintain the ordering of timeouts, so timeouts 143 // never fire before a timeout set for an earlier time, or 144 // before a timeout for the same time already submitted. 145 // See https://html.spec.whatwg.org/#dom-settimeout #16 and #17 146 while (RefPtr<Timeout> timeout = mIdleTimeouts.GetLast()) { 147 if (num == 0) { 148 when = timeout->When(); 149 } 150 timeout->remove(); 151 mTimeouts.InsertFront(timeout); 152 if (profiler_thread_is_being_profiled_for_markers()) { 153 if (num == 0) { 154 now = TimeStamp::Now(); 155 } 156 TimeDuration elapsed = now - timeout->SubmitTime(); 157 TimeDuration target = timeout->When() - timeout->SubmitTime(); 158 TimeDuration delta = now - timeout->When(); 159 if (mIsWindow) { 160 nsPrintfCString marker( 161 "Releasing deferred setTimeout() for %dms (original target time " 162 "was " 163 "%dms (%dms delta))", 164 int(elapsed.ToMilliseconds()), int(target.ToMilliseconds()), 165 int(delta.ToMilliseconds())); 166 // don't have end before start... 167 PROFILER_MARKER_TEXT( 168 "setTimeout deferred release", DOM, 169 MarkerOptions( 170 MarkerTiming::Interval( 171 delta.ToMilliseconds() >= 0 ? timeout->When() : now, now), 172 MarkerInnerWindowId( 173 mGlobalObject.GetAsInnerWindow()->WindowID())), 174 marker); 175 } 176 // TODO: add separate marker for Workers case 177 } 178 num++; 179 } 180 if (num > 0) { 181 MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(when)); 182 mIdleExecutor->Cancel(); 183 } 184 MOZ_LOG(gTimeoutLog, LogLevel::Debug, 185 ("%p: Moved %d timeouts from Idle to active", this, num)); 186 } 187 188 uint32_t TimeoutManager::CreateFiringId() { 189 uint32_t id = mNextFiringId; 190 mNextFiringId += 1; 191 if (mNextFiringId == InvalidFiringId) { 192 mNextFiringId += 1; 193 } 194 195 mFiringIdStack.AppendElement(id); 196 197 return id; 198 } 199 200 void TimeoutManager::DestroyFiringId(uint32_t aFiringId) { 201 MOZ_DIAGNOSTIC_ASSERT(!mFiringIdStack.IsEmpty()); 202 MOZ_DIAGNOSTIC_ASSERT(mFiringIdStack.LastElement() == aFiringId); 203 mFiringIdStack.RemoveLastElement(); 204 } 205 206 bool TimeoutManager::IsValidFiringId(uint32_t aFiringId) const { 207 return !IsInvalidFiringId(aFiringId); 208 } 209 210 TimeDuration TimeoutManager::MinSchedulingDelay() const { 211 if (IsActive()) { 212 return TimeDuration(); 213 } 214 215 // do not throttle workers if dom_workers_throttling is disabled 216 if (!mIsWindow && !StaticPrefs::dom_workers_throttling_enabled_AtStartup()) { 217 return TimeDuration(); 218 } 219 220 bool isBackground = mGlobalObject.IsBackgroundInternal(); 221 222 // If a window/worker isn't active as defined by TimeoutManager::IsActive() 223 // and we're throttling timeouts using an execution budget, we 224 // should adjust the minimum scheduling delay if we have used up all 225 // of our execution budget. Note that a window/worker can be active or 226 // inactive regardless of wether it is in the foreground or in the 227 // background. Throttling using a budget depends largely on the 228 // regeneration factor, which can be specified separately for 229 // foreground and background windows. 230 // 231 // The value that we compute is the time in the future when we again 232 // have a positive execution budget. We do this by taking the 233 // execution budget into account, which if it positive implies that 234 // we have time left to execute, and if it is negative implies that 235 // we should throttle it until the budget again is positive. The 236 // factor used is the rate of budget regeneration. 237 // 238 // We clamp the delay to be less than or equal to 239 // "dom.timeout.budget_throttling_max_delay" to not entirely starve 240 // the timeouts. 241 // 242 // Consider these examples assuming we should throttle using 243 // budgets: 244 // 245 // mExecutionBudget is 20ms 246 // factor is 1, which is 1 ms/ms 247 // delay is 0ms 248 // then we will compute the minimum delay: 249 // max(0, - 20 * 1) = 0 250 // 251 // mExecutionBudget is -50ms 252 // factor is 0.1, which is 1 ms/10ms 253 // delay is 1000ms 254 // then we will compute the minimum delay: 255 // max(1000, - (- 50) * 1/0.1) = max(1000, 500) = 1000 256 // 257 // mExecutionBudget is -15ms 258 // factor is 0.01, which is 1 ms/100ms 259 // delay is 1000ms 260 // then we will compute the minimum delay: 261 // max(1000, - (- 15) * 1/0.01) = max(1000, 1500) = 1500 262 TimeDuration unthrottled = 263 isBackground ? TimeDuration::FromMilliseconds( 264 StaticPrefs::dom_min_background_timeout_value()) 265 : TimeDuration(); 266 bool budgetThrottlingEnabled = BudgetThrottlingEnabled(isBackground); 267 if (budgetThrottlingEnabled && mExecutionBudget < TimeDuration()) { 268 // Only throttle if execution budget is less than 0 269 270 double factor = 1.0 / GetRegenerationFactor(isBackground); 271 return TimeDuration::Max(unthrottled, -mExecutionBudget.MultDouble(factor)); 272 } 273 if (!budgetThrottlingEnabled && isBackground) { 274 return TimeDuration::FromMilliseconds( 275 StaticPrefs:: 276 dom_min_background_timeout_value_without_budget_throttling()); 277 } 278 279 return unthrottled; 280 } 281 282 nsresult TimeoutManager::MaybeSchedule(const TimeStamp& aWhen, 283 const TimeStamp& aNow) { 284 MOZ_DIAGNOSTIC_ASSERT(mExecutor); 285 286 // Before we can schedule the executor we need to make sure that we 287 // have an updated execution budget. 288 UpdateBudget(aNow); 289 return mExecutor->MaybeSchedule(aWhen, MinSchedulingDelay()); 290 } 291 292 bool TimeoutManager::IsInvalidFiringId(uint32_t aFiringId) const { 293 // Check the most common ways to invalidate a firing id first. 294 // These should be quite fast. 295 if (aFiringId == InvalidFiringId || mFiringIdStack.IsEmpty()) { 296 return true; 297 } 298 299 if (mFiringIdStack.Length() == 1) { 300 return mFiringIdStack[0] != aFiringId; 301 } 302 303 // Next do a range check on the first and last items in the stack 304 // of active firing ids. This is a bit slower. 305 uint32_t low = mFiringIdStack[0]; 306 uint32_t high = mFiringIdStack.LastElement(); 307 MOZ_DIAGNOSTIC_ASSERT(low != high); 308 if (low > high) { 309 // If the first element is bigger than the last element in the 310 // stack, that means mNextFiringId wrapped around to zero at 311 // some point. 312 std::swap(low, high); 313 } 314 MOZ_DIAGNOSTIC_ASSERT(low < high); 315 316 if (aFiringId < low || aFiringId > high) { 317 return true; 318 } 319 320 // Finally, fall back to verifying the firing id is not anywhere 321 // in the stack. This could be slow for a large stack, but that 322 // should be rare. It can only happen with deeply nested event 323 // loop spinning. For example, a page that does a lot of timers 324 // and a lot of sync XHRs within those timers could be slow here. 325 return !mFiringIdStack.Contains(aFiringId); 326 } 327 328 TimeDuration TimeoutManager::CalculateDelay(Timeout* aTimeout) const { 329 MOZ_DIAGNOSTIC_ASSERT(aTimeout); 330 TimeDuration result = aTimeout->mInterval; 331 332 if (aTimeout->mNestingLevel >= 333 StaticPrefs::dom_clamp_timeout_nesting_level() && 334 !mIsChromeWorker) { 335 uint32_t minTimeoutValue = StaticPrefs::dom_min_timeout_value(); 336 result = TimeDuration::Max(result, 337 TimeDuration::FromMilliseconds(minTimeoutValue)); 338 } 339 340 return result; 341 } 342 343 void TimeoutManager::RecordExecution(Timeout* aRunningTimeout, 344 Timeout* aTimeout) { 345 TimeStamp now = TimeStamp::Now(); 346 TimeoutBudgetManager& budgetManager{mIsWindow ? sBudgetManager 347 : mBudgetManager}; 348 349 if (aRunningTimeout) { 350 // If we're running a timeout callback, record any execution until 351 // now. 352 TimeDuration duration = budgetManager.RecordExecution(now, aRunningTimeout); 353 354 UpdateBudget(now, duration); 355 } 356 357 if (aTimeout) { 358 // If we're starting a new timeout callback, start recording. 359 budgetManager.StartRecording(now); 360 } else { 361 // Else stop by clearing the start timestamp. 362 budgetManager.StopRecording(); 363 } 364 } 365 366 void TimeoutManager::UpdateBudget(const TimeStamp& aNow, 367 const TimeDuration& aDuration) { 368 nsGlobalWindowInner* window = GetInnerWindow(); 369 if (!window) { 370 return; 371 } 372 373 if (window->IsChromeWindow()) { 374 return; 375 } 376 377 // The budget is adjusted by increasing it with the time since the 378 // last budget update factored with the regeneration rate. If a 379 // runnable has executed, subtract that duration from the 380 // budget. The budget updated without consideration of wether the 381 // window/worker is active or not. If throttling is enabled and the 382 // window/worker is active and then becomes inactive, an overdrawn budget will 383 // still be counted against the minimum delay. 384 bool isBackground = mGlobalObject.IsBackgroundInternal(); 385 if (BudgetThrottlingEnabled(isBackground)) { 386 double factor = GetRegenerationFactor(isBackground); 387 TimeDuration regenerated = (aNow - mLastBudgetUpdate).MultDouble(factor); 388 // Clamp the budget to the range of minimum and maximum allowed budget. 389 mExecutionBudget = TimeDuration::Max( 390 GetMinBudget(isBackground), 391 TimeDuration::Min(GetMaxBudget(isBackground), 392 mExecutionBudget - aDuration + regenerated)); 393 } else { 394 // If budget throttling isn't enabled, reset the execution budget 395 // to the max budget specified in preferences. Always doing this 396 // will catch the case of BudgetThrottlingEnabled going from 397 // returning true to returning false. This prevent us from looping 398 // in RunTimeout, due to totalTimeLimit being set to zero and no 399 // timeouts being executed, even though budget throttling isn't 400 // active at the moment. 401 mExecutionBudget = GetMaxBudget(isBackground); 402 } 403 404 mLastBudgetUpdate = aNow; 405 } 406 407 // The longest interval (as PRIntervalTime) we permit, or that our 408 // timer code can handle, really. See DELAY_INTERVAL_LIMIT in 409 // nsTimerImpl.h for details. 410 #define DOM_MAX_TIMEOUT_VALUE DELAY_INTERVAL_LIMIT 411 412 uint32_t TimeoutManager::sNestingLevel = 0; 413 414 TimeoutManager::TimeoutManager(nsIGlobalObject& aHandle, 415 uint32_t aMaxIdleDeferMS, 416 nsISerialEventTarget* aEventTarget, 417 bool aIsChromeWorker) 418 : mGlobalObject(aHandle), 419 mExecutor(new TimeoutExecutor(this, false, 0)), 420 mIdleExecutor(new TimeoutExecutor(this, true, aMaxIdleDeferMS)), 421 mTimeouts(*this), 422 mTimeoutIdCounter(1), 423 mNextFiringId(InvalidFiringId + 1), 424 #ifdef DEBUG 425 mFiringIndex(0), 426 mLastFiringIndex(-1), 427 #endif 428 mRunningTimeout(nullptr), 429 mIdleTimeouts(*this), 430 mIdleCallbackTimeoutCounter(1), 431 mLastBudgetUpdate(TimeStamp::Now()), 432 mExecutionBudget(GetMaxBudget(mGlobalObject.IsBackgroundInternal())), 433 mThrottleTimeouts(false), 434 mThrottleTrackingTimeouts(false), 435 mBudgetThrottleTimeouts(false), 436 mIsLoading(false), 437 mEventTarget(aEventTarget), 438 mIsWindow(aHandle.GetAsInnerWindow()), 439 mIsChromeWorker(aIsChromeWorker) { 440 MOZ_LOG(gTimeoutLog, LogLevel::Debug, 441 ("TimeoutManager %p created, tracking bucketing %s\n", this, 442 StaticPrefs::privacy_trackingprotection_annotate_channels() 443 ? "enabled" 444 : "disabled")); 445 } 446 447 TimeoutManager::~TimeoutManager() { 448 if (mIsWindow) { 449 MOZ_DIAGNOSTIC_ASSERT(mGlobalObject.IsDying()); 450 } 451 MOZ_DIAGNOSTIC_ASSERT(!mThrottleTimeoutsTimer); 452 453 mExecutor->Shutdown(); 454 mIdleExecutor->Shutdown(); 455 456 MOZ_LOG(gTimeoutLog, LogLevel::Debug, 457 ("TimeoutManager %p destroyed\n", this)); 458 } 459 460 int32_t TimeoutManager::GetTimeoutId(Timeout::Reason aReason) { 461 int32_t timeoutId; 462 do { 463 switch (aReason) { 464 case Timeout::Reason::eIdleCallbackTimeout: 465 timeoutId = mIdleCallbackTimeoutCounter; 466 if (mIdleCallbackTimeoutCounter == 467 std::numeric_limits<int32_t>::max()) { 468 mIdleCallbackTimeoutCounter = 1; 469 } else { 470 ++mIdleCallbackTimeoutCounter; 471 } 472 break; 473 case Timeout::Reason::eTimeoutOrInterval: 474 timeoutId = mTimeoutIdCounter; 475 if (mTimeoutIdCounter == std::numeric_limits<int32_t>::max()) { 476 mTimeoutIdCounter = 1; 477 } else { 478 ++mTimeoutIdCounter; 479 } 480 break; 481 case Timeout::Reason::eDelayedWebTaskTimeout: 482 case Timeout::Reason::eJSTimeout: 483 default: 484 return -1; // no cancellation support 485 } 486 } while (mTimeouts.GetTimeout(timeoutId, aReason)); 487 488 return timeoutId; 489 } 490 491 bool TimeoutManager::IsRunningTimeout() const { return mRunningTimeout; } 492 493 nsresult TimeoutManager::SetTimeout(TimeoutHandler* aHandler, int32_t interval, 494 bool aIsInterval, Timeout::Reason aReason, 495 int32_t* aReturn) { 496 // If we don't have a document (we could have been unloaded since 497 // the call to setTimeout was made), do nothing. 498 if (mIsWindow) { 499 nsCOMPtr<Document> doc = mGlobalObject.GetAsInnerWindow()->GetExtantDoc(); 500 if (!doc || mGlobalObject.IsDying()) { 501 return NS_OK; 502 } 503 } 504 505 auto scopeExit = MakeScopeExit([&] { 506 if (!mIsWindow && !HasTimeouts()) { 507 mGlobalObject.TriggerUpdateCCFlag(); 508 } 509 }); 510 511 // Disallow negative intervals. 512 interval = std::max(0, interval); 513 514 // Make sure we don't proceed with an interval larger than our timer 515 // code can handle. (Note: we already forced |interval| to be non-negative, 516 // so the uint32_t cast (to avoid compiler warnings) is ok.) 517 uint32_t maxTimeoutMs = PR_IntervalToMilliseconds(DOM_MAX_TIMEOUT_VALUE); 518 if (static_cast<uint32_t>(interval) > maxTimeoutMs) { 519 interval = maxTimeoutMs; 520 } 521 522 RefPtr<Timeout> timeout = new Timeout(); 523 #ifdef DEBUG 524 timeout->mFiringIndex = -1; 525 #endif 526 timeout->mGlobal = &mGlobalObject; 527 timeout->mIsInterval = aIsInterval; 528 timeout->mInterval = TimeDuration::FromMilliseconds(interval); 529 timeout->mScriptHandler = aHandler; 530 timeout->mReason = aReason; 531 532 if (mIsWindow) { 533 // No popups from timeouts by default 534 timeout->mPopupState = PopupBlocker::openAbused; 535 } 536 537 // XXX: Does eIdleCallbackTimeout need clamping? 538 if (aReason == Timeout::Reason::eTimeoutOrInterval || 539 aReason == Timeout::Reason::eIdleCallbackTimeout) { 540 const uint32_t nestingLevel{mIsWindow ? GetNestingLevelForWindow() 541 : GetNestingLevelForWorker()}; 542 timeout->mNestingLevel = 543 nestingLevel < StaticPrefs::dom_clamp_timeout_nesting_level() 544 ? nestingLevel + 1 545 : nestingLevel; 546 } 547 548 // Now clamp the actual interval we will use for the timer based on 549 TimeDuration realInterval = CalculateDelay(timeout); 550 TimeStamp now = TimeStamp::Now(); 551 timeout->SetWhenOrTimeRemaining(now, realInterval); 552 553 // If we're not suspended, then set the timer. 554 if (!mGlobalObject.IsSuspended()) { 555 nsresult rv = MaybeSchedule(timeout->When(), now); 556 if (NS_FAILED(rv)) { 557 return rv; 558 } 559 } 560 561 if (mIsWindow) { 562 if (gRunningTimeoutDepth == 0 && 563 PopupBlocker::GetPopupControlState() < PopupBlocker::openBlocked) { 564 // This timeout is *not* set from another timeout and it's set 565 // while popups are enabled. Propagate the state to the timeout if 566 // its delay (interval) is equal to or less than what 567 // "dom.disable_open_click_delay" is set to (in ms). 568 569 // This is checking |interval|, not realInterval, on purpose, 570 // because our lower bound for |realInterval| could be pretty high 571 // in some cases. 572 if (interval <= StaticPrefs::dom_disable_open_click_delay()) { 573 timeout->mPopupState = PopupBlocker::GetPopupControlState(); 574 } 575 } 576 } 577 578 Timeouts::SortBy sort(mGlobalObject.IsFrozen() 579 ? Timeouts::SortBy::TimeRemaining 580 : Timeouts::SortBy::TimeWhen); 581 582 timeout->mTimeoutId = GetTimeoutId(aReason); 583 mTimeouts.Insert(timeout, sort); 584 585 *aReturn = timeout->mTimeoutId; 586 587 MOZ_LOG( 588 gTimeoutLog, LogLevel::Debug, 589 ("Set%s(TimeoutManager=%p, timeout=%p, delay=%i, " 590 "minimum=%f, throttling=%s, state=%s(%s), realInterval=%f) " 591 "returned timeout ID %u, budget=%d\n", 592 aIsInterval ? "Interval" : "Timeout", this, timeout.get(), interval, 593 (CalculateDelay(timeout) - timeout->mInterval).ToMilliseconds(), 594 mThrottleTimeouts ? "yes" : (mThrottleTimeoutsTimer ? "pending" : "no"), 595 IsActive() ? "active" : "inactive", 596 mGlobalObject.IsBackgroundInternal() ? "background" : "foreground", 597 realInterval.ToMilliseconds(), timeout->mTimeoutId, 598 int(mExecutionBudget.ToMilliseconds()))); 599 600 return NS_OK; 601 } 602 603 // Make sure we clear it no matter which list it's in 604 void TimeoutManager::ClearTimeout(int32_t aTimerId, Timeout::Reason aReason) { 605 if (ClearTimeoutInternal(aTimerId, aReason, false) || 606 mIdleTimeouts.IsEmpty()) { 607 return; // no need to check the other list if we cleared the timeout 608 } 609 ClearTimeoutInternal(aTimerId, aReason, true); 610 } 611 612 bool TimeoutManager::ClearTimeoutInternal(int32_t aTimerId, 613 Timeout::Reason aReason, 614 bool aIsIdle) { 615 MOZ_ASSERT(aReason == Timeout::Reason::eTimeoutOrInterval || 616 aReason == Timeout::Reason::eIdleCallbackTimeout, 617 "This timeout reason doesn't support cancellation."); 618 619 Timeouts& timeouts = aIsIdle ? mIdleTimeouts : mTimeouts; 620 RefPtr<TimeoutExecutor>& executor = aIsIdle ? mIdleExecutor : mExecutor; 621 bool deferredDeletion = false; 622 623 Timeout* timeout = timeouts.GetTimeout(aTimerId, aReason); 624 if (!timeout) { 625 return false; 626 } 627 bool firstTimeout = timeout == timeouts.GetFirst(); 628 629 MOZ_LOG(gTimeoutLog, LogLevel::Debug, 630 ("%s(TimeoutManager=%p, timeout=%p, ID=%u)\n", 631 timeout->mReason == Timeout::Reason::eIdleCallbackTimeout 632 ? "CancelIdleCallback" 633 : timeout->mIsInterval ? "ClearInterval" 634 : "ClearTimeout", 635 this, timeout, timeout->mTimeoutId)); 636 637 if (timeout->mRunning) { 638 /* We're running from inside the timeout. Mark this 639 timeout for deferred deletion by the code in 640 RunTimeout() */ 641 timeout->mIsInterval = false; 642 deferredDeletion = true; 643 } else { 644 /* Delete the aTimeout from the pending aTimeout list */ 645 timeout->remove(); 646 } 647 648 // We don't need to reschedule the executor if any of the following are true: 649 // * If the we weren't cancelling the first timeout, then the executor's 650 // state doesn't need to change. It will only reflect the next soonest 651 // Timeout. 652 // * If we did cancel the first Timeout, but its currently running, then 653 // RunTimeout() will handle rescheduling the executor. 654 // * If the window/worker has become suspended then we should not start 655 // executing 656 // Timeouts. 657 if (!firstTimeout || deferredDeletion || (mGlobalObject.IsSuspended())) { 658 return true; 659 } 660 661 // Stop the executor and restart it at the next soonest deadline. 662 executor->Cancel(); 663 664 Timeout* nextTimeout = timeouts.GetFirst(); 665 if (nextTimeout) { 666 if (aIsIdle) { 667 MOZ_ALWAYS_SUCCEEDS( 668 executor->MaybeSchedule(nextTimeout->When(), TimeDuration(0))); 669 } else { 670 MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextTimeout->When())); 671 } 672 } 673 return true; 674 } 675 676 void TimeoutManager::RunTimeout(const TimeStamp& aNow, 677 const TimeStamp& aTargetDeadline, 678 bool aProcessIdle) { 679 MOZ_DIAGNOSTIC_ASSERT(!aNow.IsNull()); 680 MOZ_DIAGNOSTIC_ASSERT(!aTargetDeadline.IsNull()); 681 682 nsCOMPtr<nsIGlobalObject> global = &mGlobalObject; 683 684 MOZ_ASSERT_IF(mGlobalObject.IsFrozen(), mGlobalObject.IsSuspended()); 685 686 if (mGlobalObject.IsSuspended()) { 687 return; 688 } 689 690 if (!GetInnerWindow()) { 691 // Workers don't use TaskController at the moment, so all the 692 // runnables have the same priorities. So we special case it 693 // here to allow "higher" prority tasks to run first before 694 // timers. 695 if (mGlobalObject.HasScheduledNormalOrHighPriorityWebTasks()) { 696 MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(aNow)); 697 return; 698 } 699 } 700 701 Timeouts& timeouts(aProcessIdle ? mIdleTimeouts : mTimeouts); 702 703 // Limit the overall time spent in RunTimeout() to reduce jank. 704 uint32_t totalTimeLimitMS = 705 std::max(1u, StaticPrefs::dom_timeout_max_consecutive_callbacks_ms()); 706 const TimeDuration totalTimeLimit = 707 TimeDuration::Min(TimeDuration::FromMilliseconds(totalTimeLimitMS), 708 TimeDuration::Max(TimeDuration(), mExecutionBudget)); 709 710 // Allow up to 25% of our total time budget to be used figuring out which 711 // timers need to run. This is the initial loop in this method. 712 const TimeDuration initialTimeLimit = 713 TimeDuration::FromMilliseconds(totalTimeLimit.ToMilliseconds() / 4); 714 715 // Ammortize overhead from from calling TimeStamp::Now() in the initial 716 // loop, though, by only checking for an elapsed limit every N timeouts. 717 const uint32_t kNumTimersPerInitialElapsedCheck = 100; 718 719 // Start measuring elapsed time immediately. We won't potentially expire 720 // the time budget until at least one Timeout has run, though. 721 TimeStamp now(aNow); 722 TimeStamp start = now; 723 724 uint32_t firingId = CreateFiringId(); 725 auto guard = MakeScopeExit([&] { DestroyFiringId(firingId); }); 726 727 // Accessing members of mGlobalObject here is safe, because the lifetime of 728 // TimeoutManager is the same as the lifetime of the containing 729 // nsGlobalWindow. 730 731 // A native timer has gone off. See which of our timeouts need 732 // servicing 733 TimeStamp deadline; 734 735 if (aTargetDeadline > now) { 736 // The OS timer fired early (which can happen due to the timers 737 // having lower precision than TimeStamp does). Set |deadline| to 738 // be the time when the OS timer *should* have fired so that any 739 // timers that *should* have fired *will* be fired now. 740 741 deadline = aTargetDeadline; 742 } else { 743 deadline = now; 744 } 745 746 TimeStamp nextDeadline; 747 uint32_t numTimersToRun = 0; 748 749 // The timeout list is kept in deadline order. Discover the latest timeout 750 // whose deadline has expired. On some platforms, native timeout events fire 751 // "early", but we handled that above by setting deadline to aTargetDeadline 752 // if the timer fired early. So we can stop walking if we get to timeouts 753 // whose When() is greater than deadline, since once that happens we know 754 // nothing past that point is expired. 755 756 for (Timeout* timeout = timeouts.GetFirst(); timeout != nullptr; 757 timeout = timeout->getNext()) { 758 if (totalTimeLimit.IsZero() || timeout->When() > deadline) { 759 nextDeadline = timeout->When(); 760 break; 761 } 762 763 if (IsInvalidFiringId(timeout->mFiringId)) { 764 // Mark any timeouts that are on the list to be fired with the 765 // firing depth so that we can reentrantly run timeouts 766 timeout->mFiringId = firingId; 767 768 numTimersToRun += 1; 769 770 // Run only a limited number of timers based on the configured maximum. 771 if (numTimersToRun % kNumTimersPerInitialElapsedCheck == 0) { 772 now = TimeStamp::Now(); 773 TimeDuration elapsed(now - start); 774 if (elapsed >= initialTimeLimit) { 775 nextDeadline = timeout->When(); 776 break; 777 } 778 } 779 } 780 } 781 if (aProcessIdle) { 782 MOZ_LOG( 783 gTimeoutLog, LogLevel::Debug, 784 ("Running %u deferred timeouts on idle (TimeoutManager=%p), " 785 "nextDeadline = %gms from now", 786 numTimersToRun, this, 787 nextDeadline.IsNull() ? 0.0 : (nextDeadline - now).ToMilliseconds())); 788 } 789 790 now = TimeStamp::Now(); 791 792 // Wherever we stopped in the timer list, schedule the executor to 793 // run for the next unexpired deadline. Note, this *must* be done 794 // before we start executing any content script handlers. If one 795 // of them spins the event loop the executor must already be scheduled 796 // in order for timeouts to fire properly. 797 if (!nextDeadline.IsNull()) { 798 // Note, we verified the window/worker is not suspended at the top of 799 // method and the window/worker should not have been suspended while 800 // executing the loop above since it doesn't call out to js. 801 MOZ_DIAGNOSTIC_ASSERT(!mGlobalObject.IsSuspended()); 802 if (aProcessIdle) { 803 // We don't want to update timing budget for idle queue firings, and 804 // all timeouts in the IdleTimeouts list have hit their deadlines, 805 // and so should run as soon as possible. 806 MOZ_ALWAYS_SUCCEEDS( 807 mIdleExecutor->MaybeSchedule(nextDeadline, TimeDuration())); 808 } else { 809 MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextDeadline, now)); 810 } 811 } 812 813 // Maybe the timeout that the event was fired for has been deleted 814 // and there are no others timeouts with deadlines that make them 815 // eligible for execution yet. Go away. 816 if (!numTimersToRun) { 817 return; 818 } 819 820 // Now we need to search the normal and tracking timer list at the same 821 // time to run the timers in the scheduled order. 822 823 // We stop iterating each list when we go past the last expired timeout from 824 // that list that we have observed above. That timeout will either be the 825 // next item after the last timeout we looked at or nullptr if we have 826 // exhausted the entire list while looking for the last expired timeout. 827 { 828 // Use a nested scope in order to make sure the strong references held while 829 // iterating are freed after the loop. 830 831 // The next timeout to run. This is used to advance the loop, but 832 // we cannot set it until we've run the current timeout, since 833 // running the current timeout might remove the immediate next 834 // timeout. 835 RefPtr<Timeout> next; 836 837 for (RefPtr<Timeout> timeout = timeouts.GetFirst(); timeout != nullptr; 838 timeout = next) { 839 next = timeout->getNext(); 840 // We should only execute callbacks for the set of expired Timeout 841 // objects we computed above. 842 if (timeout->mFiringId != firingId) { 843 // If the FiringId does not match, but is still valid, then this is 844 // a Timeout for another RunTimeout() on the call stack (such as in 845 // the case of nested event loops, for alert() or more likely XHR). 846 // Just skip it. 847 if (IsValidFiringId(timeout->mFiringId)) { 848 MOZ_LOG(gTimeoutLog, LogLevel::Debug, 849 ("Skipping Run%s(TimeoutManager=%p, timeout=%p) since " 850 "firingId %d is valid (processing firingId %d)" 851 #ifdef DEBUG 852 " - FiringIndex %" PRId64 " (mLastFiringIndex %" PRId64 ")" 853 #endif 854 , 855 timeout->mIsInterval ? "Interval" : "Timeout", this, 856 timeout.get(), timeout->mFiringId, firingId 857 #ifdef DEBUG 858 , 859 timeout->mFiringIndex, mFiringIndex 860 #endif 861 )); 862 #ifdef DEBUG 863 // The old FiringIndex assumed no recursion; recursion can cause 864 // other timers to get fired "in the middle" of a sequence we've 865 // already assigned firingindexes to. Since we're not going to 866 // run this timeout now, remove any FiringIndex that was already 867 // set. 868 869 // Since all timers that have FiringIndexes set *must* be ready 870 // to run and have valid FiringIds, all of them will be 'skipped' 871 // and reset if we recurse - we don't have to look through the 872 // list past where we'll stop on the first InvalidFiringId. 873 timeout->mFiringIndex = -1; 874 #endif 875 continue; 876 } 877 878 // If, however, the FiringId is invalid then we have reached Timeout 879 // objects beyond the list we calculated above. This can happen 880 // if the Timeout just beyond our last expired Timeout is cancelled 881 // by one of the callbacks we've just executed. In this case we 882 // should just stop iterating. We're done. 883 else { 884 break; 885 } 886 } 887 888 MOZ_ASSERT_IF(mGlobalObject.IsFrozen(), mGlobalObject.IsSuspended()); 889 if (mGlobalObject.IsSuspended()) { 890 break; 891 } 892 893 // The timeout is on the list to run at this depth, go ahead and 894 // process it. 895 896 if (mIsLoading && !aProcessIdle) { 897 // Any timeouts that would fire during a load will be deferred 898 // until the load event occurs, but if there's an idle time, 899 // they'll be run before the load event. 900 timeout->remove(); 901 // MOZ_RELEASE_ASSERT(timeout->When() <= (TimeStamp::Now())); 902 mIdleTimeouts.InsertBack(timeout); 903 if (MOZ_LOG_TEST(gTimeoutLog, LogLevel::Debug)) { 904 uint32_t num = 0; 905 for (Timeout* t = mIdleTimeouts.GetFirst(); t != nullptr; 906 t = t->getNext()) { 907 num++; 908 } 909 MOZ_LOG( 910 gTimeoutLog, LogLevel::Debug, 911 ("Deferring Run%s(TimeoutManager=%p, timeout=%p (%gms in the " 912 "past)) (%u deferred)", 913 timeout->mIsInterval ? "Interval" : "Timeout", this, 914 timeout.get(), (now - timeout->When()).ToMilliseconds(), num)); 915 } 916 MOZ_ALWAYS_SUCCEEDS(mIdleExecutor->MaybeSchedule(now, TimeDuration())); 917 } else { 918 // Record the first time we try to fire a timeout, and ensure that 919 // all actual firings occur in that order. This ensures that we 920 // retain compliance with the spec language 921 // (https://html.spec.whatwg.org/#dom-settimeout) specifically items 922 // 15 ("If method context is a Window object, wait until the Document 923 // associated with method context has been fully active for a further 924 // timeout milliseconds (not necessarily consecutively)") and item 16 925 // ("Wait until any invocations of this algorithm that had the same 926 // method context, that started before this one, and whose timeout is 927 // equal to or less than this one's, have completed."). 928 #ifdef DEBUG 929 if (timeout->mFiringIndex == -1) { 930 timeout->mFiringIndex = mFiringIndex++; 931 } 932 #endif 933 934 if (mGlobalObject.IsDying()) { 935 timeout->remove(); 936 continue; 937 } 938 939 #ifdef DEBUG 940 if (timeout->mFiringIndex <= mLastFiringIndex) { 941 MOZ_LOG(gTimeoutLog, LogLevel::Debug, 942 ("Incorrect firing index for Run%s(TimeoutManager=%p, " 943 "timeout=%p) with " 944 "firingId %d - FiringIndex %" PRId64 945 " (mLastFiringIndex %" PRId64 ")", 946 timeout->mIsInterval ? "Interval" : "Timeout", this, 947 timeout.get(), timeout->mFiringId, timeout->mFiringIndex, 948 mFiringIndex)); 949 } 950 MOZ_ASSERT(timeout->mFiringIndex > mLastFiringIndex); 951 mLastFiringIndex = timeout->mFiringIndex; 952 #endif 953 // This timeout is good to run. 954 bool timeout_was_cleared = false; 955 956 timeout_was_cleared = global->RunTimeoutHandler(timeout); 957 958 MOZ_LOG(gTimeoutLog, LogLevel::Debug, 959 ("Run%s(TimeoutManager=%p, timeout=%p) returned %d\n", 960 timeout->mIsInterval ? "Interval" : "Timeout", this, 961 timeout.get(), !!timeout_was_cleared)); 962 963 if (timeout_was_cleared) { 964 // Make sure we're not holding any Timeout objects alive. 965 next = nullptr; 966 967 // Since ClearAllTimeouts() was called the lists should be empty. 968 MOZ_DIAGNOSTIC_ASSERT(!HasTimeouts()); 969 970 return; 971 } 972 973 // If we need to reschedule a setInterval() the delay should be 974 // calculated based on when its callback started to execute. So 975 // save off the last time before updating our "now" timestamp to 976 // account for its callback execution time. 977 TimeStamp lastCallbackTime = now; 978 now = TimeStamp::Now(); 979 980 // If we have a regular interval timer, we re-schedule the 981 // timeout, accounting for clock drift. 982 bool needsReinsertion = 983 RescheduleTimeout(timeout, lastCallbackTime, now); 984 985 // Running a timeout can cause another timeout to be deleted, so 986 // we need to reset the pointer to the following timeout. 987 next = timeout->getNext(); 988 989 timeout->remove(); 990 991 if (needsReinsertion) { 992 // Insert interval timeout onto the corresponding list sorted in 993 // deadline order. AddRefs timeout. 994 // Always re-insert into the normal time queue! 995 mTimeouts.Insert(timeout, mGlobalObject.IsFrozen() 996 ? Timeouts::SortBy::TimeRemaining 997 : Timeouts::SortBy::TimeWhen); 998 } 999 } 1000 // Check to see if we have run out of time to execute timeout handlers. 1001 // If we've exceeded our time budget then terminate the loop immediately. 1002 // 1003 // Or if there are high priority tasks dispatched by the Scheduler API, 1004 // they should run first before timers. 1005 TimeDuration elapsed = now - start; 1006 if (elapsed >= totalTimeLimit || 1007 mGlobalObject.HasScheduledNormalOrHighPriorityWebTasks()) { 1008 // We ran out of time. Make sure to schedule the executor to 1009 // run immediately for the next timer, if it exists. Its possible, 1010 // however, that the last timeout handler suspended the window. If 1011 // that happened then we must skip this step. 1012 if (!mGlobalObject.IsSuspended()) { 1013 if (next) { 1014 if (aProcessIdle) { 1015 // We don't want to update timing budget for idle queue firings, 1016 // and all timeouts in the IdleTimeouts list have hit their 1017 // deadlines, and so should run as soon as possible. 1018 1019 // Shouldn't need cancelling since it never waits 1020 MOZ_ALWAYS_SUCCEEDS( 1021 mIdleExecutor->MaybeSchedule(next->When(), TimeDuration())); 1022 } else { 1023 // If we ran out of execution budget we need to force a 1024 // reschedule. By cancelling the executor we will not run 1025 // immediately, but instead reschedule to the minimum 1026 // scheduling delay. 1027 if (mExecutionBudget < TimeDuration()) { 1028 mExecutor->Cancel(); 1029 } 1030 1031 MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(next->When(), now)); 1032 } 1033 } 1034 } 1035 break; 1036 } 1037 } 1038 } 1039 } 1040 1041 bool TimeoutManager::RescheduleTimeout(Timeout* aTimeout, 1042 const TimeStamp& aLastCallbackTime, 1043 const TimeStamp& aCurrentNow) { 1044 MOZ_DIAGNOSTIC_ASSERT(aLastCallbackTime <= aCurrentNow); 1045 1046 if (!aTimeout->mIsInterval) { 1047 return false; 1048 } 1049 1050 // Automatically increase the nesting level when a setInterval() 1051 // is rescheduled just as if it was using a chained setTimeout(). 1052 if (aTimeout->mNestingLevel < 1053 StaticPrefs::dom_clamp_timeout_nesting_level()) { 1054 aTimeout->mNestingLevel += 1; 1055 } 1056 1057 // Compute time to next timeout for interval timer. 1058 // Make sure nextInterval is at least CalculateDelay(). 1059 TimeDuration nextInterval = CalculateDelay(aTimeout); 1060 1061 TimeStamp firingTime = aLastCallbackTime + nextInterval; 1062 TimeDuration delay = firingTime - aCurrentNow; 1063 1064 #ifdef DEBUG 1065 aTimeout->mFiringIndex = -1; 1066 #endif 1067 // And make sure delay is nonnegative; that might happen if the timer 1068 // thread is firing our timers somewhat early or if they're taking a long 1069 // time to run the callback. 1070 if (delay < TimeDuration(0)) { 1071 delay = TimeDuration(0); 1072 } 1073 1074 aTimeout->SetWhenOrTimeRemaining(aCurrentNow, delay); 1075 1076 if (mGlobalObject.IsSuspended()) { 1077 return true; 1078 } 1079 1080 nsresult rv = MaybeSchedule(aTimeout->When(), aCurrentNow); 1081 NS_ENSURE_SUCCESS(rv, false); 1082 1083 return true; 1084 } 1085 1086 void TimeoutManager::ClearAllTimeouts() { 1087 bool seenRunningTimeout = false; 1088 1089 MOZ_LOG(gTimeoutLog, LogLevel::Debug, 1090 ("ClearAllTimeouts(TimeoutManager=%p)\n", this)); 1091 1092 if (mThrottleTimeoutsTimer) { 1093 mThrottleTimeoutsTimer->Cancel(); 1094 mThrottleTimeoutsTimer = nullptr; 1095 } 1096 1097 mExecutor->Cancel(); 1098 mIdleExecutor->Cancel(); 1099 1100 ForEachUnorderedTimeout([&](Timeout* aTimeout) { 1101 /* If RunTimeout() is higher up on the stack for this 1102 window, e.g. as a result of document.write from a timeout, 1103 then we need to reset the list insertion point for 1104 newly-created timeouts in case the user adds a timeout, 1105 before we pop the stack back to RunTimeout. */ 1106 if (mRunningTimeout == aTimeout) { 1107 seenRunningTimeout = true; 1108 } 1109 1110 // Set timeout->mCleared to true to indicate that the timeout was 1111 // cleared and taken out of the list of timeouts 1112 aTimeout->mCleared = true; 1113 }); 1114 1115 // Clear out our lists 1116 mTimeouts.Clear(); 1117 mIdleTimeouts.Clear(); 1118 } 1119 1120 void TimeoutManager::Timeouts::Insert(Timeout* aTimeout, SortBy aSortBy) { 1121 // Start at mLastTimeout and go backwards. Stop if we see a Timeout with a 1122 // valid FiringId since those timers are currently being processed by 1123 // RunTimeout. This optimizes for the common case of insertion at the end. 1124 Timeout* prevSibling; 1125 for (prevSibling = GetLast(); 1126 prevSibling && 1127 // This condition needs to match the one in SetTimeoutOrInterval that 1128 // determines whether to set When() or TimeRemaining(). 1129 (aSortBy == SortBy::TimeRemaining 1130 ? prevSibling->TimeRemaining() > aTimeout->TimeRemaining() 1131 : prevSibling->When() > aTimeout->When()) && 1132 // Check the firing ID last since it will evaluate true in the vast 1133 // majority of cases. 1134 mManager.IsInvalidFiringId(prevSibling->mFiringId); 1135 prevSibling = prevSibling->getPrevious()) { 1136 /* Do nothing; just searching */ 1137 } 1138 1139 // Now link in aTimeout after prevSibling. 1140 if (prevSibling) { 1141 aTimeout->SetTimeoutContainer(mTimeouts); 1142 prevSibling->setNext(aTimeout); 1143 } else { 1144 InsertFront(aTimeout); 1145 } 1146 1147 aTimeout->mFiringId = InvalidFiringId; 1148 } 1149 1150 Timeout* TimeoutManager::BeginRunningTimeout(Timeout* aTimeout) { 1151 Timeout* currentTimeout = mRunningTimeout; 1152 mRunningTimeout = aTimeout; 1153 if (mIsWindow) { 1154 ++gRunningTimeoutDepth; 1155 } 1156 1157 RecordExecution(currentTimeout, aTimeout); 1158 return currentTimeout; 1159 } 1160 1161 void TimeoutManager::EndRunningTimeout(Timeout* aTimeout) { 1162 if (mIsWindow) { 1163 --gRunningTimeoutDepth; 1164 } 1165 1166 RecordExecution(mRunningTimeout, aTimeout); 1167 mRunningTimeout = aTimeout; 1168 } 1169 1170 void TimeoutManager::UnmarkGrayTimers() { 1171 ForEachUnorderedTimeout([](Timeout* aTimeout) { 1172 if (aTimeout->mScriptHandler) { 1173 aTimeout->mScriptHandler->MarkForCC(); 1174 } 1175 }); 1176 } 1177 1178 void TimeoutManager::Suspend() { 1179 MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Suspend(TimeoutManager=%p)\n", this)); 1180 1181 if (mThrottleTimeoutsTimer) { 1182 mThrottleTimeoutsTimer->Cancel(); 1183 mThrottleTimeoutsTimer = nullptr; 1184 } 1185 1186 mExecutor->Cancel(); 1187 mIdleExecutor->Cancel(); 1188 } 1189 1190 void TimeoutManager::Resume() { 1191 MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Resume(TimeoutManager=%p)\n", this)); 1192 nsGlobalWindowInner* window = GetInnerWindow(); 1193 1194 // When Suspend() has been called after IsDocumentLoaded(), but the 1195 // throttle tracking timer never managed to fire, start the timer 1196 // again. 1197 if (window && window->IsDocumentLoaded() && !mThrottleTimeouts) { 1198 MaybeStartThrottleTimeout(); 1199 } 1200 1201 Timeout* nextTimeout = mTimeouts.GetFirst(); 1202 if (nextTimeout) { 1203 MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextTimeout->When())); 1204 } 1205 nextTimeout = mIdleTimeouts.GetFirst(); 1206 if (nextTimeout) { 1207 MOZ_ALWAYS_SUCCEEDS( 1208 mIdleExecutor->MaybeSchedule(nextTimeout->When(), TimeDuration())); 1209 } 1210 } 1211 1212 void TimeoutManager::Freeze() { 1213 MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Freeze(TimeoutManager=%p)\n", this)); 1214 1215 // When freezing, preemptively move timeouts from the idle timeout queue to 1216 // the normal queue. This way they get scheduled automatically when we thaw. 1217 // We don't need to cancel the idle executor here, since that is done in 1218 // Suspend. 1219 size_t num = 0; 1220 while (RefPtr<Timeout> timeout = mIdleTimeouts.GetLast()) { 1221 num++; 1222 timeout->remove(); 1223 mTimeouts.InsertFront(timeout); 1224 } 1225 1226 MOZ_LOG(gTimeoutLog, LogLevel::Debug, 1227 ("%p: Moved %zu (frozen) timeouts from Idle to active", this, num)); 1228 1229 TimeStamp now = TimeStamp::Now(); 1230 ForEachUnorderedTimeout([&](Timeout* aTimeout) { 1231 // Save the current remaining time for this timeout. We will 1232 // re-apply it when the window is Thaw()'d. This effectively 1233 // shifts timers to the right as if time does not pass while 1234 // the window is frozen. 1235 TimeDuration delta(0); 1236 if (aTimeout->When() > now) { 1237 delta = aTimeout->When() - now; 1238 } 1239 aTimeout->SetWhenOrTimeRemaining(now, delta); 1240 MOZ_DIAGNOSTIC_ASSERT(aTimeout->TimeRemaining() == delta); 1241 }); 1242 } 1243 1244 void TimeoutManager::Thaw() { 1245 MOZ_LOG(gTimeoutLog, LogLevel::Debug, ("Thaw(TimeoutManager=%p)\n", this)); 1246 1247 TimeStamp now = TimeStamp::Now(); 1248 1249 ForEachUnorderedTimeout([&](Timeout* aTimeout) { 1250 // Set When() back to the time when the timer is supposed to fire. 1251 aTimeout->SetWhenOrTimeRemaining(now, aTimeout->TimeRemaining()); 1252 MOZ_DIAGNOSTIC_ASSERT(!aTimeout->When().IsNull()); 1253 }); 1254 } 1255 1256 void TimeoutManager::UpdateBackgroundState() { 1257 mExecutionBudget = GetMaxBudget(mGlobalObject.IsBackgroundInternal()); 1258 1259 // When the window/worker moves to the background or foreground we should 1260 // reschedule the TimeoutExecutor in case the MinSchedulingDelay() 1261 // changed. Only do this if the window/worker is not suspended and we 1262 // actually have a timeout. 1263 if (!mGlobalObject.IsSuspended()) { 1264 Timeout* nextTimeout = mTimeouts.GetFirst(); 1265 if (nextTimeout) { 1266 mExecutor->Cancel(); 1267 MOZ_ALWAYS_SUCCEEDS(MaybeSchedule(nextTimeout->When())); 1268 } 1269 // the Idle queue should all be past their firing time, so there we just 1270 // need to restart the queue 1271 1272 // XXX May not be needed if we don't stop the idle queue, as 1273 // MinSchedulingDelay isn't relevant here 1274 nextTimeout = mIdleTimeouts.GetFirst(); 1275 if (nextTimeout) { 1276 mIdleExecutor->Cancel(); 1277 MOZ_ALWAYS_SUCCEEDS( 1278 mIdleExecutor->MaybeSchedule(nextTimeout->When(), TimeDuration())); 1279 } 1280 } 1281 } 1282 1283 namespace { 1284 1285 class ThrottleTimeoutsCallback final : public nsITimerCallback, 1286 public nsINamed { 1287 public: 1288 explicit ThrottleTimeoutsCallback(nsIGlobalObject* aHandle) 1289 : mGlobalObject(aHandle) {} 1290 1291 NS_DECL_ISUPPORTS 1292 NS_DECL_NSITIMERCALLBACK 1293 1294 NS_IMETHOD GetName(nsACString& aName) override { 1295 aName.AssignLiteral("ThrottleTimeoutsCallback"); 1296 return NS_OK; 1297 } 1298 1299 private: 1300 ~ThrottleTimeoutsCallback() = default; 1301 1302 private: 1303 // The strong reference here keeps the Window/worker and hence the 1304 // TimeoutManager object itself alive. 1305 RefPtr<nsIGlobalObject> mGlobalObject; 1306 }; 1307 1308 NS_IMPL_ISUPPORTS(ThrottleTimeoutsCallback, nsITimerCallback, nsINamed) 1309 1310 NS_IMETHODIMP 1311 ThrottleTimeoutsCallback::Notify(nsITimer* aTimer) { 1312 if (mGlobalObject) { 1313 mGlobalObject->GetTimeoutManager()->StartThrottlingTimeouts(); 1314 } 1315 mGlobalObject = nullptr; 1316 return NS_OK; 1317 } 1318 1319 } // namespace 1320 1321 bool TimeoutManager::BudgetThrottlingEnabled(bool aIsBackground) const { 1322 // do not throttle workers if dom_workers_throttling is disabled 1323 if (!mIsWindow && !StaticPrefs::dom_workers_throttling_enabled_AtStartup()) { 1324 return false; 1325 } 1326 1327 // A window/worker can be throttled using budget if 1328 // * It isn't active 1329 // * If it isn't using WebRTC 1330 // * If it hasn't got open WebSockets 1331 // * If it hasn't got active IndexedDB databases 1332 1333 // Note that we allow both foreground and background to be 1334 // considered for budget throttling. What determines if they are if 1335 // budget throttling is enabled is the max budget. 1336 if ((aIsBackground 1337 ? StaticPrefs::dom_timeout_background_throttling_max_budget() 1338 : StaticPrefs::dom_timeout_foreground_throttling_max_budget()) < 0) { 1339 return false; 1340 } 1341 1342 if (!mBudgetThrottleTimeouts || IsActive()) { 1343 return false; 1344 } 1345 1346 // Check if there are any active IndexedDB databases 1347 // TODO: mGlobalObject must implement HasActiveIndexedDBDatabases() 1348 // Not implemented yet in workers 1349 if (mGlobalObject.HasActiveIndexedDBDatabases()) { 1350 // TODO: A window/worker can be throttled using budget if mGlobalObject has 1351 // active IndexedDB Databases 1352 // Not implemented yet in workers 1353 return false; 1354 } 1355 1356 // Check if we have active PeerConnection 1357 // TODO: mGlobalObject must implement HasActivePeerConnections() 1358 if (mGlobalObject.HasActivePeerConnections()) { 1359 // TODO: A window/worker can be throttled using budget if mGlobalObject has 1360 // active peer connections 1361 // Not implemented yet in workers 1362 return false; 1363 } 1364 1365 if (mGlobalObject.HasOpenWebSockets()) { 1366 // TODO: A window/worker can be throttled using budget if mGlobalObject has 1367 // open web sockets 1368 // Not implemented yet in workers 1369 return false; 1370 } 1371 1372 return true; 1373 } 1374 1375 void TimeoutManager::StartThrottlingTimeouts() { 1376 MOZ_ASSERT(NS_IsMainThread()); 1377 MOZ_DIAGNOSTIC_ASSERT(mThrottleTimeoutsTimer); 1378 1379 MOZ_LOG(gTimeoutLog, LogLevel::Debug, 1380 ("TimeoutManager %p started to throttle tracking timeouts\n", this)); 1381 1382 MOZ_DIAGNOSTIC_ASSERT(!mThrottleTimeouts); 1383 mThrottleTimeouts = true; 1384 mThrottleTrackingTimeouts = true; 1385 mBudgetThrottleTimeouts = 1386 StaticPrefs::dom_timeout_enable_budget_timer_throttling(); 1387 mThrottleTimeoutsTimer = nullptr; 1388 } 1389 1390 void TimeoutManager::OnDocumentLoaded() { 1391 // The load event may be firing again if we're coming back to the page by 1392 // navigating through the session history, so we need to ensure to only call 1393 // this when mThrottleTimeouts hasn't been set yet. 1394 if (!mThrottleTimeouts) { 1395 MaybeStartThrottleTimeout(); 1396 } 1397 } 1398 1399 void TimeoutManager::MaybeStartThrottleTimeout() { 1400 if (StaticPrefs::dom_timeout_throttling_delay() <= 0 || 1401 mGlobalObject.IsDying() || mGlobalObject.IsSuspended()) { 1402 return; 1403 } 1404 1405 MOZ_DIAGNOSTIC_ASSERT(!mThrottleTimeouts); 1406 1407 MOZ_LOG(gTimeoutLog, LogLevel::Debug, 1408 ("TimeoutManager %p delaying tracking timeout throttling by %dms\n", 1409 this, StaticPrefs::dom_timeout_throttling_delay())); 1410 1411 nsCOMPtr<nsITimerCallback> callback = 1412 new ThrottleTimeoutsCallback(&mGlobalObject); 1413 1414 NS_NewTimerWithCallback(getter_AddRefs(mThrottleTimeoutsTimer), callback, 1415 StaticPrefs::dom_timeout_throttling_delay(), 1416 nsITimer::TYPE_ONE_SHOT, EventTarget()); 1417 } 1418 1419 void TimeoutManager::BeginSyncOperation() { 1420 // If we're beginning a sync operation, the currently running 1421 // timeout will be put on hold. To not get into an inconsistent 1422 // state, where the currently running timeout appears to take time 1423 // equivalent to the period of us spinning up a new event loop, 1424 // record what we have and stop recording until we reach 1425 // EndSyncOperation. 1426 RecordExecution(mRunningTimeout, nullptr); 1427 } 1428 1429 void TimeoutManager::EndSyncOperation() { 1430 // If we're running a timeout, restart the measurement from here. 1431 RecordExecution(nullptr, mRunningTimeout); 1432 } 1433 1434 nsIEventTarget* TimeoutManager::EventTarget() { return mEventTarget; }