зеркало из https://github.com/mozilla/gecko-dev.git
1361 строка
49 KiB
C++
1361 строка
49 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "TimeoutManager.h"
|
|
#include "nsGlobalWindow.h"
|
|
#include "mozilla/Logging.h"
|
|
#include "mozilla/ThrottledEventQueue.h"
|
|
#include "mozilla/TimeStamp.h"
|
|
#include "nsITimeoutHandler.h"
|
|
#include "mozilla/dom/TabGroup.h"
|
|
#include "OrderedTimeoutIterator.h"
|
|
|
|
using namespace mozilla;
|
|
using namespace mozilla::dom;
|
|
|
|
static LazyLogModule gLog("Timeout");
|
|
|
|
static int32_t gRunningTimeoutDepth = 0;
|
|
|
|
// The default shortest interval/timeout we permit
|
|
#define DEFAULT_MIN_TIMEOUT_VALUE 4 // 4ms
|
|
#define DEFAULT_MIN_BACKGROUND_TIMEOUT_VALUE 1000 // 1000ms
|
|
#define DEFAULT_MIN_TRACKING_TIMEOUT_VALUE 4 // 4ms
|
|
#define DEFAULT_MIN_TRACKING_BACKGROUND_TIMEOUT_VALUE 1000 // 1000ms
|
|
static int32_t gMinTimeoutValue = 0;
|
|
static int32_t gMinBackgroundTimeoutValue = 0;
|
|
static int32_t gMinTrackingTimeoutValue = 0;
|
|
static int32_t gMinTrackingBackgroundTimeoutValue = 0;
|
|
static int32_t gTrackingTimeoutThrottlingDelay = 0;
|
|
static bool gAnnotateTrackingChannels = false;
|
|
int32_t
|
|
TimeoutManager::DOMMinTimeoutValue(bool aIsTracking) const {
|
|
// First apply any back pressure delay that might be in effect.
|
|
int32_t value = std::max(mBackPressureDelayMS, 0);
|
|
// Don't use the background timeout value when the tab is playing audio.
|
|
// Until bug 1336484 we only used to do this for pages that use Web Audio.
|
|
// The original behavior was implemented in bug 11811073.
|
|
bool isBackground = !mWindow.AsInner()->IsPlayingAudio() &&
|
|
mWindow.IsBackgroundInternal();
|
|
bool throttleTracking = aIsTracking && mThrottleTrackingTimeouts;
|
|
auto minValue = throttleTracking ? (isBackground ? gMinTrackingBackgroundTimeoutValue
|
|
: gMinTrackingTimeoutValue)
|
|
: (isBackground ? gMinBackgroundTimeoutValue
|
|
: gMinTimeoutValue);
|
|
return std::max(minValue, value);
|
|
}
|
|
|
|
#define TRACKING_SEPARATE_TIMEOUT_BUCKETING_STRATEGY 0 // Consider all timeouts coming from tracking scripts as tracking
|
|
// These strategies are useful for testing.
|
|
#define ALL_NORMAL_TIMEOUT_BUCKETING_STRATEGY 1 // Consider all timeouts as normal
|
|
#define ALTERNATE_TIMEOUT_BUCKETING_STRATEGY 2 // Put every other timeout in the list of tracking timeouts
|
|
#define RANDOM_TIMEOUT_BUCKETING_STRATEGY 3 // Put timeouts into either the normal or tracking timeouts list randomly
|
|
static int32_t gTimeoutBucketingStrategy = 0;
|
|
|
|
#define DEFAULT_TRACKING_TIMEOUT_THROTTLING_DELAY -1 // Only positive integers cause us to introduce a delay for tracking
|
|
// timeout throttling.
|
|
|
|
// The number of nested timeouts before we start clamping. HTML5 says 1, WebKit
|
|
// uses 5.
|
|
#define DOM_CLAMP_TIMEOUT_NESTING_LEVEL 5
|
|
|
|
// The longest interval (as PRIntervalTime) we permit, or that our
|
|
// timer code can handle, really. See DELAY_INTERVAL_LIMIT in
|
|
// nsTimerImpl.h for details.
|
|
#define DOM_MAX_TIMEOUT_VALUE DELAY_INTERVAL_LIMIT
|
|
|
|
uint32_t TimeoutManager::sNestingLevel = 0;
|
|
|
|
namespace {
|
|
|
|
// The maximum number of timer callbacks we will try to run in a single event
|
|
// loop runnable.
|
|
#define DEFAULT_TARGET_MAX_CONSECUTIVE_CALLBACKS 5
|
|
uint32_t gTargetMaxConsecutiveCallbacks;
|
|
|
|
// The number of queued runnables within the TabGroup ThrottledEventQueue
|
|
// at which to begin applying back pressure to the window.
|
|
#define DEFAULT_THROTTLED_EVENT_QUEUE_BACK_PRESSURE 5000
|
|
static uint32_t gThrottledEventQueueBackPressure;
|
|
|
|
// The amount of delay to apply to timers when back pressure is triggered.
|
|
// As the length of the ThrottledEventQueue grows delay is increased. The
|
|
// delay is scaled such that every kThrottledEventQueueBackPressure runnables
|
|
// in the queue equates to an additional kBackPressureDelayMS.
|
|
#define DEFAULT_BACK_PRESSURE_DELAY_MS 250
|
|
static uint32_t gBackPressureDelayMS;
|
|
|
|
// This defines a limit for how much the delay must drop before we actually
|
|
// reduce back pressure throttle amount. This makes the throttle delay
|
|
// a bit "sticky" once we enter back pressure.
|
|
#define DEFAULT_BACK_PRESSURE_DELAY_REDUCTION_THRESHOLD_MS 1000
|
|
static uint32_t gBackPressureDelayReductionThresholdMS;
|
|
|
|
// The minimum delay we can reduce back pressure to before we just floor
|
|
// the value back to zero. This allows us to ensure that we can exit
|
|
// back pressure event if there are always a small number of runnables
|
|
// queued up.
|
|
#define DEFAULT_BACK_PRESSURE_DELAY_MINIMUM_MS 100
|
|
static uint32_t gBackPressureDelayMinimumMS;
|
|
|
|
// Convert a ThrottledEventQueue length to a timer delay in milliseconds.
|
|
// This will return a value between 0 and INT32_MAX.
|
|
int32_t
|
|
CalculateNewBackPressureDelayMS(uint32_t aBacklogDepth)
|
|
{
|
|
double multiplier = static_cast<double>(aBacklogDepth) /
|
|
static_cast<double>(gThrottledEventQueueBackPressure);
|
|
double value = static_cast<double>(gBackPressureDelayMS) * multiplier;
|
|
// Avoid overflow
|
|
if (value > INT32_MAX) {
|
|
value = INT32_MAX;
|
|
}
|
|
|
|
// Once we get close to an empty queue just floor the delay back to zero.
|
|
// We want to ensure we don't get stuck in a condition where there is a
|
|
// small amount of delay remaining due to an active, but reasonable, queue.
|
|
else if (value < static_cast<double>(gBackPressureDelayMinimumMS)) {
|
|
value = 0;
|
|
}
|
|
return static_cast<int32_t>(value);
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
TimeoutManager::TimeoutManager(nsGlobalWindow& aWindow)
|
|
: mWindow(aWindow),
|
|
mTimeoutIdCounter(1),
|
|
mTimeoutFiringDepth(0),
|
|
mRunningTimeout(nullptr),
|
|
mIdleCallbackTimeoutCounter(1),
|
|
mBackPressureDelayMS(0),
|
|
mThrottleTrackingTimeouts(gTrackingTimeoutThrottlingDelay <= 0)
|
|
{
|
|
MOZ_DIAGNOSTIC_ASSERT(aWindow.IsInnerWindow());
|
|
|
|
MOZ_LOG(gLog, LogLevel::Debug,
|
|
("TimeoutManager %p created, tracking bucketing %s\n",
|
|
this, gAnnotateTrackingChannels ? "enabled" : "disabled"));
|
|
}
|
|
|
|
TimeoutManager::~TimeoutManager()
|
|
{
|
|
MOZ_DIAGNOSTIC_ASSERT(!mThrottleTrackingTimeoutsTimer);
|
|
|
|
MOZ_LOG(gLog, LogLevel::Debug,
|
|
("TimeoutManager %p destroyed\n", this));
|
|
}
|
|
|
|
/* static */
|
|
void
|
|
TimeoutManager::Initialize()
|
|
{
|
|
Preferences::AddIntVarCache(&gMinTimeoutValue,
|
|
"dom.min_timeout_value",
|
|
DEFAULT_MIN_TIMEOUT_VALUE);
|
|
Preferences::AddIntVarCache(&gMinBackgroundTimeoutValue,
|
|
"dom.min_background_timeout_value",
|
|
DEFAULT_MIN_BACKGROUND_TIMEOUT_VALUE);
|
|
Preferences::AddIntVarCache(&gMinTrackingTimeoutValue,
|
|
"dom.min_tracking_timeout_value",
|
|
DEFAULT_MIN_TRACKING_TIMEOUT_VALUE);
|
|
Preferences::AddIntVarCache(&gMinTrackingBackgroundTimeoutValue,
|
|
"dom.min_tracking_background_timeout_value",
|
|
DEFAULT_MIN_TRACKING_BACKGROUND_TIMEOUT_VALUE);
|
|
Preferences::AddIntVarCache(&gTimeoutBucketingStrategy,
|
|
"dom.timeout_bucketing_strategy",
|
|
TRACKING_SEPARATE_TIMEOUT_BUCKETING_STRATEGY);
|
|
Preferences::AddIntVarCache(&gTrackingTimeoutThrottlingDelay,
|
|
"dom.timeout.tracking_throttling_delay",
|
|
DEFAULT_TRACKING_TIMEOUT_THROTTLING_DELAY);
|
|
Preferences::AddBoolVarCache(&gAnnotateTrackingChannels,
|
|
"privacy.trackingprotection.annotate_channels",
|
|
false);
|
|
|
|
Preferences::AddUintVarCache(&gThrottledEventQueueBackPressure,
|
|
"dom.timeout.throttled_event_queue_back_pressure",
|
|
DEFAULT_THROTTLED_EVENT_QUEUE_BACK_PRESSURE);
|
|
Preferences::AddUintVarCache(&gBackPressureDelayMS,
|
|
"dom.timeout.back_pressure_delay_ms",
|
|
DEFAULT_BACK_PRESSURE_DELAY_MS);
|
|
Preferences::AddUintVarCache(&gBackPressureDelayReductionThresholdMS,
|
|
"dom.timeout.back_pressure_delay_reduction_threshold_ms",
|
|
DEFAULT_BACK_PRESSURE_DELAY_REDUCTION_THRESHOLD_MS);
|
|
Preferences::AddUintVarCache(&gBackPressureDelayMinimumMS,
|
|
"dom.timeout.back_pressure_delay_minimum_ms",
|
|
DEFAULT_BACK_PRESSURE_DELAY_MINIMUM_MS);
|
|
|
|
Preferences::AddUintVarCache(&gTargetMaxConsecutiveCallbacks,
|
|
"dom.timeout.max_consecutive_callbacks",
|
|
DEFAULT_TARGET_MAX_CONSECUTIVE_CALLBACKS);
|
|
}
|
|
|
|
uint32_t
|
|
TimeoutManager::GetTimeoutId(Timeout::Reason aReason)
|
|
{
|
|
switch (aReason) {
|
|
case Timeout::Reason::eIdleCallbackTimeout:
|
|
return ++mIdleCallbackTimeoutCounter;
|
|
case Timeout::Reason::eTimeoutOrInterval:
|
|
default:
|
|
return ++mTimeoutIdCounter;
|
|
}
|
|
}
|
|
|
|
nsresult
|
|
TimeoutManager::SetTimeout(nsITimeoutHandler* aHandler,
|
|
int32_t interval, bool aIsInterval,
|
|
Timeout::Reason aReason, int32_t* aReturn)
|
|
{
|
|
// If we don't have a document (we could have been unloaded since
|
|
// the call to setTimeout was made), do nothing.
|
|
nsCOMPtr<nsIDocument> doc = mWindow.GetExtantDoc();
|
|
if (!doc) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// Disallow negative intervals. If aIsInterval also disallow 0,
|
|
// because we use that as a "don't repeat" flag.
|
|
interval = std::max(aIsInterval ? 1 : 0, interval);
|
|
|
|
// Make sure we don't proceed with an interval larger than our timer
|
|
// code can handle. (Note: we already forced |interval| to be non-negative,
|
|
// so the uint32_t cast (to avoid compiler warnings) is ok.)
|
|
uint32_t maxTimeoutMs = PR_IntervalToMilliseconds(DOM_MAX_TIMEOUT_VALUE);
|
|
if (static_cast<uint32_t>(interval) > maxTimeoutMs) {
|
|
interval = maxTimeoutMs;
|
|
}
|
|
|
|
RefPtr<Timeout> timeout = new Timeout();
|
|
timeout->mIsInterval = aIsInterval;
|
|
timeout->mInterval = interval;
|
|
timeout->mScriptHandler = aHandler;
|
|
timeout->mReason = aReason;
|
|
|
|
switch (gTimeoutBucketingStrategy) {
|
|
default:
|
|
case TRACKING_SEPARATE_TIMEOUT_BUCKETING_STRATEGY: {
|
|
const char* filename = nullptr;
|
|
uint32_t dummyLine = 0, dummyColumn = 0;
|
|
aHandler->GetLocation(&filename, &dummyLine, &dummyColumn);
|
|
timeout->mIsTracking = doc->IsScriptTracking(nsDependentCString(filename));
|
|
|
|
MOZ_LOG(gLog, LogLevel::Debug,
|
|
("Classified timeout %p set from %s as %stracking\n",
|
|
timeout.get(), filename, timeout->mIsTracking ? "" : "non-"));
|
|
break;
|
|
}
|
|
case ALL_NORMAL_TIMEOUT_BUCKETING_STRATEGY:
|
|
// timeout->mIsTracking is already false!
|
|
MOZ_DIAGNOSTIC_ASSERT(!timeout->mIsTracking);
|
|
|
|
MOZ_LOG(gLog, LogLevel::Debug,
|
|
("Classified timeout %p unconditionally as normal\n",
|
|
timeout.get()));
|
|
break;
|
|
case ALTERNATE_TIMEOUT_BUCKETING_STRATEGY:
|
|
timeout->mIsTracking = (mTimeoutIdCounter % 2) == 0;
|
|
|
|
MOZ_LOG(gLog, LogLevel::Debug,
|
|
("Classified timeout %p as %stracking (alternating mode)\n",
|
|
timeout.get(), timeout->mIsTracking ? "" : "non-"));
|
|
break;
|
|
case RANDOM_TIMEOUT_BUCKETING_STRATEGY:
|
|
timeout->mIsTracking = (rand() % 2) == 0;
|
|
|
|
MOZ_LOG(gLog, LogLevel::Debug,
|
|
("Classified timeout %p as %stracking (random mode)\n",
|
|
timeout.get(), timeout->mIsTracking ? "" : "non-"));
|
|
break;
|
|
}
|
|
|
|
// Now clamp the actual interval we will use for the timer based on
|
|
uint32_t nestingLevel = sNestingLevel + 1;
|
|
uint32_t realInterval = interval;
|
|
if (aIsInterval || nestingLevel >= DOM_CLAMP_TIMEOUT_NESTING_LEVEL ||
|
|
mBackPressureDelayMS > 0 || mWindow.IsBackgroundInternal() ||
|
|
timeout->mIsTracking) {
|
|
// Don't allow timeouts less than DOMMinTimeoutValue() from
|
|
// now...
|
|
realInterval = std::max(realInterval,
|
|
uint32_t(DOMMinTimeoutValue(timeout->mIsTracking)));
|
|
}
|
|
|
|
timeout->mWindow = &mWindow;
|
|
|
|
TimeDuration delta = TimeDuration::FromMilliseconds(realInterval);
|
|
timeout->SetWhenOrTimeRemaining(TimeStamp::Now(), delta);
|
|
|
|
// If we're not suspended, then set the timer.
|
|
if (!mWindow.IsSuspended()) {
|
|
MOZ_ASSERT(!timeout->When().IsNull());
|
|
|
|
nsresult rv;
|
|
timeout->mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
|
|
if (NS_FAILED(rv)) {
|
|
return rv;
|
|
}
|
|
|
|
RefPtr<Timeout> copy = timeout;
|
|
|
|
rv = timeout->InitTimer(mWindow.EventTargetFor(TaskCategory::Timer),
|
|
realInterval);
|
|
if (NS_FAILED(rv)) {
|
|
return rv;
|
|
}
|
|
|
|
// The timeout is now also held in the timer's closure.
|
|
Unused << copy.forget();
|
|
}
|
|
|
|
if (!aIsInterval) {
|
|
timeout->mNestingLevel = nestingLevel;
|
|
}
|
|
|
|
// No popups from timeouts by default
|
|
timeout->mPopupState = openAbused;
|
|
|
|
if (gRunningTimeoutDepth == 0 &&
|
|
mWindow.GetPopupControlState() < openAbused) {
|
|
// This timeout is *not* set from another timeout and it's set
|
|
// while popups are enabled. Propagate the state to the timeout if
|
|
// its delay (interval) is equal to or less than what
|
|
// "dom.disable_open_click_delay" is set to (in ms).
|
|
|
|
int32_t delay =
|
|
Preferences::GetInt("dom.disable_open_click_delay");
|
|
|
|
// This is checking |interval|, not realInterval, on purpose,
|
|
// because our lower bound for |realInterval| could be pretty high
|
|
// in some cases.
|
|
if (interval <= delay) {
|
|
timeout->mPopupState = mWindow.GetPopupControlState();
|
|
}
|
|
}
|
|
|
|
Timeouts::SortBy sort(mWindow.IsFrozen() ? Timeouts::SortBy::TimeRemaining
|
|
: Timeouts::SortBy::TimeWhen);
|
|
if (timeout->mIsTracking) {
|
|
mTrackingTimeouts.Insert(timeout, sort);
|
|
} else {
|
|
mNormalTimeouts.Insert(timeout, sort);
|
|
}
|
|
|
|
timeout->mTimeoutId = GetTimeoutId(aReason);
|
|
*aReturn = timeout->mTimeoutId;
|
|
|
|
MOZ_LOG(gLog, LogLevel::Debug,
|
|
("Set%s(TimeoutManager=%p, timeout=%p, delay=%i, "
|
|
"minimum=%i, throttling=%s, background=%d, realInterval=%i) "
|
|
"returned %stracking timeout ID %u\n",
|
|
aIsInterval ? "Interval" : "Timeout",
|
|
this, timeout.get(), interval,
|
|
DOMMinTimeoutValue(timeout->mIsTracking),
|
|
mThrottleTrackingTimeouts ? "yes"
|
|
: (mThrottleTrackingTimeoutsTimer ?
|
|
"pending" : "no"),
|
|
int(mWindow.IsBackgroundInternal()), realInterval,
|
|
timeout->mIsTracking ? "" : "non-",
|
|
timeout->mTimeoutId));
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
TimeoutManager::ClearTimeout(int32_t aTimerId, Timeout::Reason aReason)
|
|
{
|
|
uint32_t timerId = (uint32_t)aTimerId;
|
|
|
|
ForEachUnorderedTimeoutAbortable([&](Timeout* aTimeout) {
|
|
MOZ_LOG(gLog, LogLevel::Debug,
|
|
("Clear%s(TimeoutManager=%p, timeout=%p, aTimerId=%u, ID=%u, tracking=%d)\n", aTimeout->mIsInterval ? "Interval" : "Timeout",
|
|
this, aTimeout, timerId, aTimeout->mTimeoutId,
|
|
int(aTimeout->mIsTracking)));
|
|
|
|
if (aTimeout->mTimeoutId == timerId && aTimeout->mReason == aReason) {
|
|
if (aTimeout->mRunning) {
|
|
/* We're running from inside the aTimeout. Mark this
|
|
aTimeout for deferred deletion by the code in
|
|
RunTimeout() */
|
|
aTimeout->mIsInterval = false;
|
|
}
|
|
else {
|
|
/* Delete the aTimeout from the pending aTimeout list */
|
|
aTimeout->remove();
|
|
|
|
if (aTimeout->mTimer) {
|
|
aTimeout->mTimer->Cancel();
|
|
aTimeout->mTimer = nullptr;
|
|
aTimeout->Release();
|
|
}
|
|
aTimeout->Release();
|
|
}
|
|
return true; // abort!
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
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);
|
|
|
|
uint32_t numTimersToRun = 0;
|
|
bool targetTimerSeen = false;
|
|
|
|
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;
|
|
}
|
|
|
|
numTimersToRun += 1;
|
|
|
|
// Note that we have seen our target timer. This means we can now
|
|
// stop processing timers once we hit our threshold below.
|
|
if (timeout == aTimeout) {
|
|
targetTimerSeen = true;
|
|
}
|
|
|
|
// Run only a limited number of timers based on the configured
|
|
// maximum. Note, we must always run our target timer however.
|
|
// 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 (targetTimerSeen &&
|
|
numTimersToRun >= gTargetMaxConsecutiveCallbacks &&
|
|
!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);
|
|
MOZ_LOG(gLog, LogLevel::Debug,
|
|
("Run%s(TimeoutManager=%p, timeout=%p, aTimeout=%p, tracking=%d) returned %d\n", timeout->mIsInterval ? "Interval" : "Timeout",
|
|
this, timeout, aTimeout,
|
|
int(aTimeout->mIsTracking),
|
|
!!timeout_was_cleared));
|
|
|
|
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();
|
|
}
|
|
|
|
void
|
|
TimeoutManager::MaybeApplyBackPressure()
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
|
|
// If we are already in back pressure then we don't need to apply back
|
|
// pressure again. We also shouldn't need to apply back pressure while
|
|
// the window is suspended.
|
|
if (mBackPressureDelayMS > 0 || mWindow.IsSuspended()) {
|
|
return;
|
|
}
|
|
|
|
RefPtr<ThrottledEventQueue> queue =
|
|
do_QueryObject(mWindow.TabGroup()->EventTargetFor(TaskCategory::Timer));
|
|
if (!queue) {
|
|
return;
|
|
}
|
|
|
|
// Only begin back pressure if the window has greatly fallen behind the main
|
|
// thread. This is a somewhat arbitrary threshold chosen such that it should
|
|
// rarely fire under normaly circumstances. Its low enough, though,
|
|
// that we should have time to slow new runnables from being added before an
|
|
// OOM occurs.
|
|
if (queue->Length() < gThrottledEventQueueBackPressure) {
|
|
return;
|
|
}
|
|
|
|
// First attempt to dispatch a runnable to update our back pressure state. We
|
|
// do this first in order to verify we can dispatch successfully before
|
|
// entering the back pressure state.
|
|
nsCOMPtr<nsIRunnable> r =
|
|
NewNonOwningRunnableMethod<StoreRefPtrPassByPtr<nsGlobalWindow>>(this,
|
|
&TimeoutManager::CancelOrUpdateBackPressure, &mWindow);
|
|
nsresult rv = queue->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
|
|
NS_ENSURE_SUCCESS_VOID(rv);
|
|
|
|
// Since the callback was scheduled successfully we can now persist the
|
|
// backpressure value.
|
|
mBackPressureDelayMS = CalculateNewBackPressureDelayMS(queue->Length());
|
|
|
|
MOZ_LOG(gLog, LogLevel::Debug,
|
|
("Applying %dms of back pressure to TimeoutManager %p "
|
|
"because of a queue length of %u\n",
|
|
mBackPressureDelayMS, this,
|
|
queue->Length()));
|
|
}
|
|
|
|
void
|
|
TimeoutManager::CancelOrUpdateBackPressure(nsGlobalWindow* aWindow)
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
MOZ_ASSERT(aWindow == &mWindow);
|
|
MOZ_ASSERT(mBackPressureDelayMS > 0);
|
|
|
|
// First, re-calculate the back pressure delay.
|
|
RefPtr<ThrottledEventQueue> queue =
|
|
do_QueryObject(mWindow.TabGroup()->EventTargetFor(TaskCategory::Timer));
|
|
auto queueLength = queue ? queue->Length() : 0;
|
|
int32_t newBackPressureDelayMS = CalculateNewBackPressureDelayMS(queueLength);
|
|
|
|
MOZ_LOG(gLog, LogLevel::Debug,
|
|
("Updating back pressure from %d to %dms for TimeoutManager %p "
|
|
"because of a queue length of %u\n",
|
|
mBackPressureDelayMS, newBackPressureDelayMS,
|
|
this, queueLength));
|
|
|
|
// If the delay has increased, then simply apply it. Increasing the delay
|
|
// does not risk re-ordering timers with similar parameters. We want to
|
|
// extra careful not to re-order sequential calls to setTimeout(func, 0),
|
|
// for example.
|
|
if (newBackPressureDelayMS > mBackPressureDelayMS) {
|
|
mBackPressureDelayMS = newBackPressureDelayMS;
|
|
}
|
|
|
|
// If the delay has decreased, though, we only apply the new value if it has
|
|
// reduced significantly. This hysteresis avoids thrashing the back pressure
|
|
// value back and forth rapidly. This is important because reducing the
|
|
// backpressure delay requires calling ResetTimerForThrottleReduction() which
|
|
// can be quite expensive. We only want to call that method if the back log
|
|
// is really clearing.
|
|
else if (newBackPressureDelayMS == 0 ||
|
|
(static_cast<uint32_t>(mBackPressureDelayMS) >
|
|
(newBackPressureDelayMS + gBackPressureDelayReductionThresholdMS))) {
|
|
int32_t oldBackPressureDelayMS = mBackPressureDelayMS;
|
|
mBackPressureDelayMS = newBackPressureDelayMS;
|
|
|
|
// If the back pressure delay has gone down we must reset any existing
|
|
// timers to use the new value. Otherwise we run the risk of executing
|
|
// timer callbacks out-of-order.
|
|
ResetTimersForThrottleReduction(oldBackPressureDelayMS);
|
|
}
|
|
|
|
// If all of the back pressure delay has been removed then we no longer need
|
|
// to check back pressure updates. We can simply return without scheduling
|
|
// another update runnable.
|
|
if (!mBackPressureDelayMS) {
|
|
return;
|
|
}
|
|
|
|
// Otherwise, if there is a back pressure delay still in effect we need
|
|
// queue a runnable to check if it can be reduced in the future. Note
|
|
// that this runnable is dispatched to the ThrottledEventQueue. This
|
|
// means we will not check for a new value until the current back log
|
|
// has been processed. The next update will only keep back pressure if
|
|
// more runnables continue to be dispatched to the queue.
|
|
nsCOMPtr<nsIRunnable> r =
|
|
NewNonOwningRunnableMethod<StoreRefPtrPassByPtr<nsGlobalWindow>>(this,
|
|
&TimeoutManager::CancelOrUpdateBackPressure, &mWindow);
|
|
MOZ_ALWAYS_SUCCEEDS(queue->Dispatch(r.forget(), NS_DISPATCH_NORMAL));
|
|
}
|
|
|
|
bool
|
|
TimeoutManager::RescheduleTimeout(Timeout* aTimeout, const TimeStamp& now,
|
|
bool aRunningPendingTimeouts)
|
|
{
|
|
if (!aTimeout->mIsInterval) {
|
|
if (aTimeout->mTimer) {
|
|
// The timeout still has an OS timer, and it's not an interval,
|
|
// that means that the OS timer could still fire; cancel the OS
|
|
// timer and release its reference to the timeout.
|
|
aTimeout->mTimer->Cancel();
|
|
aTimeout->mTimer = nullptr;
|
|
aTimeout->Release();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Compute time to next timeout for interval timer.
|
|
// Make sure nextInterval is at least DOMMinTimeoutValue().
|
|
TimeDuration nextInterval =
|
|
TimeDuration::FromMilliseconds(
|
|
std::max(aTimeout->mInterval,
|
|
uint32_t(DOMMinTimeoutValue(aTimeout->mIsTracking))));
|
|
|
|
// If we're running pending timeouts, set the next interval to be
|
|
// relative to "now", and not to when the timeout that was pending
|
|
// should have fired.
|
|
TimeStamp firingTime;
|
|
if (aRunningPendingTimeouts) {
|
|
firingTime = now + nextInterval;
|
|
} else {
|
|
firingTime = aTimeout->When() + nextInterval;
|
|
}
|
|
|
|
TimeStamp currentNow = TimeStamp::Now();
|
|
TimeDuration delay = firingTime - currentNow;
|
|
|
|
// And make sure delay is nonnegative; that might happen if the timer
|
|
// thread is firing our timers somewhat early or if they're taking a long
|
|
// time to run the callback.
|
|
if (delay < TimeDuration(0)) {
|
|
delay = TimeDuration(0);
|
|
}
|
|
|
|
aTimeout->SetWhenOrTimeRemaining(currentNow, delay);
|
|
|
|
if (!aTimeout->mTimer) {
|
|
MOZ_DIAGNOSTIC_ASSERT(mWindow.IsFrozen() || mWindow.IsSuspended());
|
|
return true;
|
|
}
|
|
|
|
// Reschedule the OS timer. Don't bother returning any error codes if
|
|
// this fails since the callers of this method don't care about them.
|
|
nsresult rv = aTimeout->InitTimer(mWindow.EventTargetFor(TaskCategory::Timer),
|
|
delay.ToMilliseconds());
|
|
|
|
if (NS_FAILED(rv)) {
|
|
NS_ERROR("Error initializing timer for DOM timeout!");
|
|
|
|
// We failed to initialize the new OS timer, this timer does
|
|
// us no good here so we just cancel it (just in case) and
|
|
// null out the pointer to the OS timer, this will release the
|
|
// OS timer. As we continue executing the code below we'll end
|
|
// up deleting the timeout since it's not an interval timeout
|
|
// any more (since timeout->mTimer == nullptr).
|
|
aTimeout->mTimer->Cancel();
|
|
aTimeout->mTimer = nullptr;
|
|
|
|
// Now that the OS timer no longer has a reference to the
|
|
// timeout we need to drop that reference.
|
|
aTimeout->Release();
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
nsresult
|
|
TimeoutManager::ResetTimersForThrottleReduction()
|
|
{
|
|
return ResetTimersForThrottleReduction(gMinBackgroundTimeoutValue);
|
|
}
|
|
|
|
nsresult
|
|
TimeoutManager::ResetTimersForThrottleReduction(int32_t aPreviousThrottleDelayMS)
|
|
{
|
|
MOZ_ASSERT(aPreviousThrottleDelayMS > 0);
|
|
|
|
if (mWindow.IsFrozen() || mWindow.IsSuspended()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
Timeouts::SortBy sortBy = mWindow.IsFrozen() ? Timeouts::SortBy::TimeRemaining
|
|
: Timeouts::SortBy::TimeWhen;
|
|
|
|
nsCOMPtr<nsIEventTarget> queue = mWindow.EventTargetFor(TaskCategory::Timer);
|
|
nsresult rv = mNormalTimeouts.ResetTimersForThrottleReduction(aPreviousThrottleDelayMS,
|
|
*this,
|
|
sortBy,
|
|
queue);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
rv = mTrackingTimeouts.ResetTimersForThrottleReduction(aPreviousThrottleDelayMS,
|
|
*this,
|
|
sortBy,
|
|
queue);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
TimeoutManager::Timeouts::ResetTimersForThrottleReduction(int32_t aPreviousThrottleDelayMS,
|
|
const TimeoutManager& aTimeoutManager,
|
|
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(aTimeoutManager.
|
|
DOMMinTimeoutValue(timeout->mIsTracking))));
|
|
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();
|
|
|
|
// Since we are only reducing intervals in this method we can
|
|
// make an optimization here. If the reduction does not cause us
|
|
// to fall before our previous timeout then we do not have to
|
|
// remove and re-insert the current timeout. This is important
|
|
// because re-insertion makes this algorithm O(n^2). Since we
|
|
// will typically be shifting a lot of timers at once this
|
|
// optimization saves us a lot of work.
|
|
Timeout* prevTimeout = timeout->getPrevious();
|
|
if (prevTimeout && prevTimeout->When() > timeout->When()) {
|
|
// 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::ClearAllTimeouts()
|
|
{
|
|
bool seenRunningTimeout = false;
|
|
|
|
MOZ_LOG(gLog, LogLevel::Debug,
|
|
("ClearAllTimeouts(TimeoutManager=%p)\n", this));
|
|
|
|
if (mThrottleTrackingTimeoutsTimer) {
|
|
mThrottleTrackingTimeoutsTimer->Cancel();
|
|
mThrottleTrackingTimeoutsTimer = nullptr;
|
|
}
|
|
|
|
ForEachUnorderedTimeout([&](Timeout* aTimeout) {
|
|
/* If RunTimeout() is higher up on the stack for this
|
|
window, e.g. as a result of document.write from a timeout,
|
|
then we need to reset the list insertion point for
|
|
newly-created timeouts in case the user adds a timeout,
|
|
before we pop the stack back to RunTimeout. */
|
|
if (mRunningTimeout == aTimeout) {
|
|
seenRunningTimeout = true;
|
|
}
|
|
|
|
if (aTimeout->mTimer) {
|
|
aTimeout->mTimer->Cancel();
|
|
aTimeout->mTimer = nullptr;
|
|
|
|
// Drop the count since the timer isn't going to hold on
|
|
// anymore.
|
|
aTimeout->Release();
|
|
}
|
|
|
|
// Set timeout->mCleared to true to indicate that the timeout was
|
|
// cleared and taken out of the list of timeouts
|
|
aTimeout->mCleared = true;
|
|
|
|
// Drop the count since we're removing it from the list.
|
|
aTimeout->Release();
|
|
});
|
|
|
|
if (seenRunningTimeout) {
|
|
mNormalTimeouts.SetInsertionPoint(nullptr);
|
|
mTrackingTimeouts.SetInsertionPoint(nullptr);
|
|
}
|
|
|
|
// Clear out our list
|
|
mNormalTimeouts.Clear();
|
|
mTrackingTimeouts.Clear();
|
|
}
|
|
|
|
void
|
|
TimeoutManager::Timeouts::Insert(Timeout* aTimeout, SortBy aSortBy)
|
|
{
|
|
// Start at mLastTimeout and go backwards. Don't go further than insertion
|
|
// point, though. This optimizes for the common case of insertion at the end.
|
|
Timeout* prevSibling;
|
|
for (prevSibling = GetLast();
|
|
prevSibling && prevSibling != InsertionPoint() &&
|
|
// This condition needs to match the one in SetTimeoutOrInterval that
|
|
// determines whether to set When() or TimeRemaining().
|
|
(aSortBy == SortBy::TimeRemaining ?
|
|
prevSibling->TimeRemaining() > aTimeout->TimeRemaining() :
|
|
prevSibling->When() > aTimeout->When());
|
|
prevSibling = prevSibling->getPrevious()) {
|
|
/* Do nothing; just searching */
|
|
}
|
|
|
|
// Now link in aTimeout after prevSibling.
|
|
if (prevSibling) {
|
|
prevSibling->setNext(aTimeout);
|
|
} else {
|
|
InsertFront(aTimeout);
|
|
}
|
|
|
|
aTimeout->mFiringDepth = 0;
|
|
|
|
// Increment the timeout's reference count since it's now held on to
|
|
// by the list
|
|
aTimeout->AddRef();
|
|
}
|
|
|
|
Timeout*
|
|
TimeoutManager::BeginRunningTimeout(Timeout* aTimeout)
|
|
{
|
|
Timeout* currentTimeout = mRunningTimeout;
|
|
mRunningTimeout = aTimeout;
|
|
|
|
++gRunningTimeoutDepth;
|
|
++mTimeoutFiringDepth;
|
|
|
|
return currentTimeout;
|
|
}
|
|
|
|
void
|
|
TimeoutManager::EndRunningTimeout(Timeout* aTimeout)
|
|
{
|
|
--mTimeoutFiringDepth;
|
|
--gRunningTimeoutDepth;
|
|
|
|
mRunningTimeout = aTimeout;
|
|
}
|
|
|
|
void
|
|
TimeoutManager::UnmarkGrayTimers()
|
|
{
|
|
ForEachUnorderedTimeout([](Timeout* aTimeout) {
|
|
if (aTimeout->mScriptHandler) {
|
|
aTimeout->mScriptHandler->MarkForCC();
|
|
}
|
|
});
|
|
}
|
|
|
|
void
|
|
TimeoutManager::Suspend()
|
|
{
|
|
MOZ_LOG(gLog, LogLevel::Debug,
|
|
("Suspend(TimeoutManager=%p)\n", this));
|
|
|
|
if (mThrottleTrackingTimeoutsTimer) {
|
|
mThrottleTrackingTimeoutsTimer->Cancel();
|
|
mThrottleTrackingTimeoutsTimer = nullptr;
|
|
}
|
|
|
|
ForEachUnorderedTimeout([](Timeout* aTimeout) {
|
|
// Leave the timers with the current time remaining. This will
|
|
// cause the timers to potentially fire when the window is
|
|
// Resume()'d. Time effectively passes while suspended.
|
|
|
|
// Drop the XPCOM timer; we'll reschedule when restoring the state.
|
|
if (aTimeout->mTimer) {
|
|
aTimeout->mTimer->Cancel();
|
|
aTimeout->mTimer = nullptr;
|
|
|
|
// Drop the reference that the timer's closure had on this timeout, we'll
|
|
// add it back in Resume().
|
|
aTimeout->Release();
|
|
}
|
|
});
|
|
}
|
|
|
|
void
|
|
TimeoutManager::Resume()
|
|
{
|
|
MOZ_LOG(gLog, LogLevel::Debug,
|
|
("Resume(TimeoutManager=%p)\n", this));
|
|
|
|
// When Suspend() has been called after IsDocumentLoaded(), but the
|
|
// throttle tracking timer never managed to fire, start the timer
|
|
// again.
|
|
if (mWindow.AsInner()->IsDocumentLoaded() && !mThrottleTrackingTimeouts) {
|
|
MaybeStartThrottleTrackingTimout();
|
|
}
|
|
|
|
TimeStamp now = TimeStamp::Now();
|
|
DebugOnly<bool> _seenDummyTimeout = false;
|
|
|
|
ForEachUnorderedTimeout([&](Timeout* aTimeout) {
|
|
// There's a chance we're being called with RunTimeout on the stack in which
|
|
// case we have a dummy timeout in the list that *must not* be resumed. It
|
|
// can be identified by a null mWindow.
|
|
if (!aTimeout->mWindow) {
|
|
NS_ASSERTION(!_seenDummyTimeout, "More than one dummy timeout?!");
|
|
_seenDummyTimeout = true;
|
|
return;
|
|
}
|
|
|
|
MOZ_ASSERT(!aTimeout->mTimer);
|
|
|
|
// The timeout When() is set to the absolute time when the timer should
|
|
// fire. Recalculate the delay from now until that deadline. If the
|
|
// the deadline has already passed or falls within our minimum delay
|
|
// deadline, then clamp the resulting value to the minimum delay. The
|
|
// When() will remain at its absolute time, but we won'aTimeout fire the OS
|
|
// timer until our calculated delay has passed.
|
|
int32_t remaining = 0;
|
|
if (aTimeout->When() > now) {
|
|
remaining = static_cast<int32_t>((aTimeout->When() - now).ToMilliseconds());
|
|
}
|
|
uint32_t delay = std::max(remaining, DOMMinTimeoutValue(aTimeout->mIsTracking));
|
|
|
|
aTimeout->mTimer = do_CreateInstance("@mozilla.org/timer;1");
|
|
if (!aTimeout->mTimer) {
|
|
aTimeout->remove();
|
|
return;
|
|
}
|
|
|
|
nsresult rv = aTimeout->InitTimer(mWindow.EventTargetFor(TaskCategory::Timer),
|
|
delay);
|
|
if (NS_FAILED(rv)) {
|
|
aTimeout->mTimer = nullptr;
|
|
aTimeout->remove();
|
|
return;
|
|
}
|
|
|
|
// Add a reference for the new timer's closure.
|
|
aTimeout->AddRef();
|
|
});
|
|
}
|
|
|
|
void
|
|
TimeoutManager::Freeze()
|
|
{
|
|
MOZ_LOG(gLog, LogLevel::Debug,
|
|
("Freeze(TimeoutManager=%p)\n", this));
|
|
|
|
DebugOnly<bool> _seenDummyTimeout = false;
|
|
|
|
TimeStamp now = TimeStamp::Now();
|
|
ForEachUnorderedTimeout([&](Timeout* aTimeout) {
|
|
if (!aTimeout->mWindow) {
|
|
NS_ASSERTION(!_seenDummyTimeout, "More than one dummy timeout?!");
|
|
_seenDummyTimeout = true;
|
|
return;
|
|
}
|
|
|
|
// Save the current remaining time for this timeout. We will
|
|
// re-apply it when the window is Thaw()'d. This effectively
|
|
// shifts timers to the right as if time does not pass while
|
|
// the window is frozen.
|
|
TimeDuration delta(0);
|
|
if (aTimeout->When() > now) {
|
|
delta = aTimeout->When() - now;
|
|
}
|
|
aTimeout->SetWhenOrTimeRemaining(now, delta);
|
|
MOZ_DIAGNOSTIC_ASSERT(aTimeout->TimeRemaining() == delta);
|
|
|
|
// Since we are suspended there should be no OS timer set for
|
|
// this timeout entry.
|
|
MOZ_ASSERT(!aTimeout->mTimer);
|
|
});
|
|
}
|
|
|
|
void
|
|
TimeoutManager::Thaw()
|
|
{
|
|
MOZ_LOG(gLog, LogLevel::Debug,
|
|
("Thaw(TimeoutManager=%p)\n", this));
|
|
|
|
TimeStamp now = TimeStamp::Now();
|
|
DebugOnly<bool> _seenDummyTimeout = false;
|
|
|
|
ForEachUnorderedTimeout([&](Timeout* aTimeout) {
|
|
// There's a chance we're being called with RunTimeout on the stack in which
|
|
// case we have a dummy timeout in the list that *must not* be resumed. It
|
|
// can be identified by a null mWindow.
|
|
if (!aTimeout->mWindow) {
|
|
NS_ASSERTION(!_seenDummyTimeout, "More than one dummy timeout?!");
|
|
_seenDummyTimeout = true;
|
|
return;
|
|
}
|
|
|
|
// Set When() back to the time when the timer is supposed to fire.
|
|
aTimeout->SetWhenOrTimeRemaining(now, aTimeout->TimeRemaining());
|
|
MOZ_DIAGNOSTIC_ASSERT(!aTimeout->When().IsNull());
|
|
|
|
MOZ_ASSERT(!aTimeout->mTimer);
|
|
});
|
|
}
|
|
|
|
bool
|
|
TimeoutManager::IsTimeoutTracking(uint32_t aTimeoutId)
|
|
{
|
|
return mTrackingTimeouts.ForEachAbortable([&](Timeout* aTimeout) {
|
|
return aTimeout->mTimeoutId == aTimeoutId;
|
|
});
|
|
}
|
|
|
|
namespace {
|
|
|
|
class ThrottleTrackingTimeoutsCallback final : public nsITimerCallback
|
|
{
|
|
public:
|
|
explicit ThrottleTrackingTimeoutsCallback(nsGlobalWindow* aWindow)
|
|
: mWindow(aWindow)
|
|
{
|
|
MOZ_DIAGNOSTIC_ASSERT(aWindow->IsInnerWindow());
|
|
}
|
|
|
|
NS_DECL_ISUPPORTS
|
|
NS_DECL_NSITIMERCALLBACK
|
|
|
|
private:
|
|
~ThrottleTrackingTimeoutsCallback() {}
|
|
|
|
private:
|
|
// The strong reference here keeps the Window and hence the TimeoutManager
|
|
// object itself alive.
|
|
RefPtr<nsGlobalWindow> mWindow;
|
|
};
|
|
|
|
NS_IMPL_ISUPPORTS(ThrottleTrackingTimeoutsCallback, nsITimerCallback)
|
|
|
|
NS_IMETHODIMP
|
|
ThrottleTrackingTimeoutsCallback::Notify(nsITimer* aTimer)
|
|
{
|
|
mWindow->AsInner()->TimeoutManager().StartThrottlingTrackingTimeouts();
|
|
mWindow = nullptr;
|
|
return NS_OK;
|
|
}
|
|
|
|
}
|
|
|
|
void
|
|
TimeoutManager::StartThrottlingTrackingTimeouts()
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
MOZ_DIAGNOSTIC_ASSERT(mThrottleTrackingTimeoutsTimer);
|
|
|
|
MOZ_LOG(gLog, LogLevel::Debug,
|
|
("TimeoutManager %p started to throttle tracking timeouts\n", this));
|
|
|
|
MOZ_DIAGNOSTIC_ASSERT(!mThrottleTrackingTimeouts);
|
|
mThrottleTrackingTimeouts = true;
|
|
mThrottleTrackingTimeoutsTimer = nullptr;
|
|
}
|
|
|
|
void
|
|
TimeoutManager::OnDocumentLoaded()
|
|
{
|
|
MaybeStartThrottleTrackingTimout();
|
|
}
|
|
|
|
void
|
|
TimeoutManager::MaybeStartThrottleTrackingTimout()
|
|
{
|
|
if (gTrackingTimeoutThrottlingDelay <= 0) {
|
|
return;
|
|
}
|
|
|
|
MOZ_DIAGNOSTIC_ASSERT(!mThrottleTrackingTimeouts);
|
|
|
|
MOZ_LOG(gLog, LogLevel::Debug,
|
|
("TimeoutManager %p delaying tracking timeout throttling by %dms\n",
|
|
this, gTrackingTimeoutThrottlingDelay));
|
|
|
|
mThrottleTrackingTimeoutsTimer =
|
|
do_CreateInstance("@mozilla.org/timer;1");
|
|
if (!mThrottleTrackingTimeoutsTimer) {
|
|
return;
|
|
}
|
|
|
|
nsCOMPtr<nsITimerCallback> callback =
|
|
new ThrottleTrackingTimeoutsCallback(&mWindow);
|
|
|
|
mThrottleTrackingTimeoutsTimer->InitWithCallback(callback,
|
|
gTrackingTimeoutThrottlingDelay,
|
|
nsITimer::TYPE_ONE_SHOT);
|
|
}
|