nsresult TimeoutManager::Timeouts::ResetTimersForThrottleReduction(int32_t aPreviousThrottleDelayMS, int32_t aMinTimeoutValueMS, SortBy aSortBy, nsIEventTarget* aQueue) { TimeStamp now = TimeStamp::Now(); // If insertion point is non-null, we're in the middle of firing timers and // the timers we're planning to fire all come before insertion point; // insertion point itself is a dummy timeout with an When() that may be // semi-bogus. In that case, we don't need to do anything with insertion // point or anything before it, so should start at the timer after insertion // point, if there is one. // Otherwise, start at the beginning of the list. for (Timeout* timeout = InsertionPoint() ? InsertionPoint()->getNext() : GetFirst(); timeout; ) { // It's important that this check be <= so that we guarantee that // taking std::max with |now| won't make a quantity equal to // timeout->When() below. if (timeout->When() <= now) { timeout = timeout->getNext(); continue; } if (timeout->When() - now > TimeDuration::FromMilliseconds(aPreviousThrottleDelayMS)) { // No need to loop further. Timeouts are sorted in When() order // and the ones after this point were all set up for at least // gMinBackgroundTimeoutValue ms and hence were not clamped. break; } // We reduced our throttled delay. Re-init the timer appropriately. // Compute the interval the timer should have had if it had not been set in a // background window TimeDuration interval = TimeDuration::FromMilliseconds(std::max(timeout->mInterval, uint32_t(aMinTimeoutValueMS))); uint32_t oldIntervalMillisecs = 0; timeout->mTimer->GetDelay(&oldIntervalMillisecs); TimeDuration oldInterval = TimeDuration::FromMilliseconds(oldIntervalMillisecs); if (oldInterval > interval) { // unclamp TimeStamp firingTime = std::max(timeout->When() - oldInterval + interval, now); NS_ASSERTION(firingTime < timeout->When(), "Our firing time should strictly decrease!"); TimeDuration delay = firingTime - now; timeout->SetWhenOrTimeRemaining(now, delay); MOZ_DIAGNOSTIC_ASSERT(timeout->When() == firingTime); // Since we reset When() we need to move |timeout| to the right // place in the list so that it remains sorted by When(). // Get the pointer to the next timeout now, before we move the // current timeout in the list. Timeout* nextTimeout = timeout->getNext(); // It is safe to remove and re-insert because When() is now // strictly smaller than it used to be, so we know we'll insert // |timeout| before nextTimeout. NS_ASSERTION(!nextTimeout || timeout->When() < nextTimeout->When(), "How did that happen?"); timeout->remove(); // Insert() will addref |timeout| and reset mFiringDepth. Make sure to // undo that after calling it. uint32_t firingDepth = timeout->mFiringDepth; Insert(timeout, aSortBy); timeout->mFiringDepth = firingDepth; timeout->Release(); nsresult rv = timeout->InitTimer(aQueue, delay.ToMilliseconds()); if (NS_FAILED(rv)) { NS_WARNING("Error resetting non background timer for DOM timeout!"); return rv; } timeout = nextTimeout; } else { timeout = timeout->getNext(); } } return NS_OK; }
void TimeoutManager::RunTimeout(Timeout* aTimeout) { if (mWindow.IsSuspended()) { return; } NS_ASSERTION(!mWindow.IsFrozen(), "Timeout running on a window in the bfcache!"); Timeout* last_expired_normal_timeout = nullptr; Timeout* last_expired_tracking_timeout = nullptr; bool last_expired_timeout_is_normal = false; Timeout* last_normal_insertion_point = nullptr; Timeout* last_tracking_insertion_point = nullptr; uint32_t firingDepth = mTimeoutFiringDepth + 1; // Make sure that the window and the script context don't go away as // a result of running timeouts nsCOMPtr<nsIScriptGlobalObject> windowKungFuDeathGrip(&mWindow); // Silence the static analysis error about windowKungFuDeathGrip. Accessing // members of mWindow here is safe, because the lifetime of TimeoutManager is // the same as the lifetime of the containing nsGlobalWindow. Unused << windowKungFuDeathGrip; // A native timer has gone off. See which of our timeouts need // servicing TimeStamp now = TimeStamp::Now(); TimeStamp deadline; if (aTimeout && aTimeout->When() > now) { // The OS timer fired early (which can happen due to the timers // having lower precision than TimeStamp does). Set |deadline| to // be the time when the OS timer *should* have fired so that any // timers that *should* have fired before aTimeout *will* be fired // now. deadline = aTimeout->When(); } else { deadline = now; } // The timeout list is kept in deadline order. Discover the latest timeout // whose deadline has expired. On some platforms, native timeout events fire // "early", but we handled that above by setting deadline to aTimeout->When() // if the timer fired early. So we can stop walking if we get to timeouts // whose When() is greater than deadline, since once that happens we know // nothing past that point is expired. { // Use a nested scope in order to make sure the strong references held by // the iterator are freed after the loop. OrderedTimeoutIterator expiredIter(mNormalTimeouts, mTrackingTimeouts, nullptr, nullptr); while (true) { Timeout* timeout = expiredIter.Next(); if (!timeout || timeout->When() > deadline) { break; } if (timeout->mFiringDepth == 0) { // Mark any timeouts that are on the list to be fired with the // firing depth so that we can reentrantly run timeouts timeout->mFiringDepth = firingDepth; last_expired_timeout_is_normal = expiredIter.PickedNormalIter(); if (last_expired_timeout_is_normal) { last_expired_normal_timeout = timeout; } else { last_expired_tracking_timeout = timeout; } // Run available timers until we see our target timer. After // that, however, stop coalescing timers so we can yield the // main thread. Further timers that are ready will get picked // up by their own nsITimer runnables when they execute. // // For chrome windows, however, we do coalesce all timers and // do not yield the main thread. This is partly because we // trust chrome windows not to misbehave and partly because a // number of browser chrome tests have races that depend on this // coalescing. if (timeout == aTimeout && !mWindow.IsChromeWindow()) { break; } } expiredIter.UpdateIterator(); } } // Maybe the timeout that the event was fired for has been deleted // and there are no others timeouts with deadlines that make them // eligible for execution yet. Go away. if (!last_expired_normal_timeout && !last_expired_tracking_timeout) { return; } // Insert a dummy timeout into the list of timeouts between the // portion of the list that we are about to process now and those // timeouts that will be processed in a future call to // win_run_timeout(). This dummy timeout serves as the head of the // list for any timeouts inserted as a result of running a timeout. RefPtr<Timeout> dummy_normal_timeout = new Timeout(); dummy_normal_timeout->mFiringDepth = firingDepth; dummy_normal_timeout->SetDummyWhen(now); if (last_expired_timeout_is_normal) { last_expired_normal_timeout->setNext(dummy_normal_timeout); } RefPtr<Timeout> dummy_tracking_timeout = new Timeout(); dummy_tracking_timeout->mFiringDepth = firingDepth; dummy_tracking_timeout->SetDummyWhen(now); if (!last_expired_timeout_is_normal) { last_expired_tracking_timeout->setNext(dummy_tracking_timeout); } RefPtr<Timeout> timeoutExtraRef1(dummy_normal_timeout); RefPtr<Timeout> timeoutExtraRef2(dummy_tracking_timeout); // Now we need to search the normal and tracking timer list at the same // time to run the timers in the scheduled order. last_normal_insertion_point = mNormalTimeouts.InsertionPoint(); if (last_expired_timeout_is_normal) { // If we ever start setting insertion point to a non-dummy timeout, the logic // in ResetTimersForThrottleReduction will need to change. mNormalTimeouts.SetInsertionPoint(dummy_normal_timeout); } last_tracking_insertion_point = mTrackingTimeouts.InsertionPoint(); if (!last_expired_timeout_is_normal) { // If we ever start setting mTrackingTimeoutInsertionPoint to a non-dummy timeout, // the logic in ResetTimersForThrottleReduction will need to change. mTrackingTimeouts.SetInsertionPoint(dummy_tracking_timeout); } // We stop iterating each list when we go past the last expired timeout from // that list that we have observed above. That timeout will either be the // dummy timeout for the list that the last expired timeout came from, or it // will be the next item after the last timeout we looked at (or nullptr if // we have exhausted the entire list while looking for the last expired // timeout). { // Use a nested scope in order to make sure the strong references held by // the iterator are freed after the loop. OrderedTimeoutIterator runIter(mNormalTimeouts, mTrackingTimeouts, last_expired_normal_timeout ? last_expired_normal_timeout->getNext() : nullptr, last_expired_tracking_timeout ? last_expired_tracking_timeout->getNext() : nullptr); while (!mWindow.IsFrozen()) { Timeout* timeout = runIter.Next(); MOZ_ASSERT(timeout != dummy_normal_timeout && timeout != dummy_tracking_timeout, "We should have stopped iterating before getting to the dummy timeout"); if (!timeout) { // We have run out of timeouts! break; } runIter.UpdateIterator(); if (timeout->mFiringDepth != firingDepth) { // We skip the timeout since it's on the list to run at another // depth. continue; } if (mWindow.IsSuspended()) { // Some timer did suspend us. Make sure the // rest of the timers get executed later. timeout->mFiringDepth = 0; continue; } // The timeout is on the list to run at this depth, go ahead and // process it. // Get the script context (a strong ref to prevent it going away) // for this timeout and ensure the script language is enabled. nsCOMPtr<nsIScriptContext> scx = mWindow.GetContextInternal(); if (!scx) { // No context means this window was closed or never properly // initialized for this language. continue; } // This timeout is good to run bool timeout_was_cleared = mWindow.RunTimeoutHandler(timeout, scx); if (timeout_was_cleared) { // Make sure the iterator isn't holding any Timeout objects alive. runIter.Clear(); // The running timeout's window was cleared, this means that // ClearAllTimeouts() was called from a *nested* call, possibly // through a timeout that fired while a modal (to this window) // dialog was open or through other non-obvious paths. // Note that if the last expired timeout corresponding to each list // is null, then we should expect a refcount of two, since the // dummy timeout for this queue was never injected into it, and the // corresponding timeoutExtraRef variable hasn't been cleared yet. if (last_expired_timeout_is_normal) { MOZ_ASSERT(dummy_normal_timeout->HasRefCnt(1), "dummy_normal_timeout may leak"); MOZ_ASSERT(dummy_tracking_timeout->HasRefCnt(2), "dummy_tracking_timeout may leak"); Unused << timeoutExtraRef1.forget().take(); } else { MOZ_ASSERT(dummy_normal_timeout->HasRefCnt(2), "dummy_normal_timeout may leak"); MOZ_ASSERT(dummy_tracking_timeout->HasRefCnt(1), "dummy_tracking_timeout may leak"); Unused << timeoutExtraRef2.forget().take(); } mNormalTimeouts.SetInsertionPoint(last_normal_insertion_point); mTrackingTimeouts.SetInsertionPoint(last_tracking_insertion_point); return; } // If we have a regular interval timer, we re-schedule the // timeout, accounting for clock drift. bool needsReinsertion = RescheduleTimeout(timeout, now, !aTimeout); // Running a timeout can cause another timeout to be deleted, so // we need to reset the pointer to the following timeout. runIter.UpdateIterator(); timeout->remove(); if (needsReinsertion) { // Insert interval timeout onto the corresponding list sorted in // deadline order. AddRefs timeout. if (runIter.PickedTrackingIter()) { mTrackingTimeouts.Insert(timeout, mWindow.IsFrozen() ? Timeouts::SortBy::TimeRemaining : Timeouts::SortBy::TimeWhen); } else { mNormalTimeouts.Insert(timeout, mWindow.IsFrozen() ? Timeouts::SortBy::TimeRemaining : Timeouts::SortBy::TimeWhen); } } // Release the timeout struct since it's possibly out of the list timeout->Release(); } } // Take the dummy timeout off the head of the list if (dummy_normal_timeout->isInList()) { dummy_normal_timeout->remove(); } timeoutExtraRef1 = nullptr; MOZ_ASSERT(dummy_normal_timeout->HasRefCnt(1), "dummy_normal_timeout may leak"); if (dummy_tracking_timeout->isInList()) { dummy_tracking_timeout->remove(); } timeoutExtraRef2 = nullptr; MOZ_ASSERT(dummy_tracking_timeout->HasRefCnt(1), "dummy_tracking_timeout may leak"); mNormalTimeouts.SetInsertionPoint(last_normal_insertion_point); mTrackingTimeouts.SetInsertionPoint(last_tracking_insertion_point); MaybeApplyBackPressure(); }