Bug 1342854 Run a limited number of timer callbacks in the same event loop runnable. r=smaug a=jcristau
authorBen Kelly <ben@wanderview.com>
Tue, 28 Feb 2017 09:40:40 -0500
changeset 500273 c9cd431edad441c7340a4d89daf30620a6e992e7
parent 500272 805bda20f2efd6e85ab37ab815c76c81e88291ba
child 500274 f644b087fe465fc747bb4db7deb646f7b842ebe2
push id49672
push userbmo:snorp@snorp.net
push dateThu, 16 Mar 2017 23:59:55 +0000
reviewerssmaug, jcristau
bugs1342854
milestone52.0
Bug 1342854 Run a limited number of timer callbacks in the same event loop runnable. r=smaug a=jcristau
dom/base/nsGlobalWindow.cpp
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -1222,16 +1222,21 @@ NewOuterWindowProxy(JSContext *cx, JS::H
                                             : &nsOuterWindowProxy::singleton,
                                    options);
   MOZ_ASSERT_IF(obj, js::IsWindowProxy(obj));
   return obj;
 }
 
 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
@@ -1380,16 +1385,20 @@ nsGlobalWindow::nsGlobalWindow(nsGlobalW
                                  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);
+
     sFirstTime = false;
   }
 
   if (gDumpFile == nullptr) {
     const nsAdoptingCString& fname =
       Preferences::GetCString("browser.dom.window.dump.file");
     if (!fname.IsEmpty()) {
       // if this fails to open, Dump() knows to just go to stdout
@@ -13176,45 +13185,59 @@ nsGlobalWindow::RunTimeout(Timeout* aTim
     // timers that *should* have fired before aTimeout *will* be fired
     // now.
 
     deadline = aTimeout->mWhen;
   } else {
     deadline = now;
   }
 
+  uint32_t numTimersToRun = 0;
+  bool targetTimerSeen = false;
+
+
   // 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->mWhen
   // if the timer fired early.  So we can stop walking if we get to timeouts
   // whose mWhen is greater than deadline, since once that happens we know
   // nothing past that point is expired.
   last_expired_timeout = nullptr;
   for (Timeout* timeout = mTimeouts.getFirst();
        timeout && timeout->mWhen <= deadline;
        timeout = timeout->getNext()) {
     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 = 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.
+      // 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 (timeout == aTimeout && !IsChromeWindow()) {
+      if (targetTimerSeen &&
+          numTimersToRun >= gTargetMaxConsecutiveCallbacks &&
+          !IsChromeWindow()) {
         break;
       }
+
+      numTimersToRun += 1;
     }
   }
 
   // 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_timeout) {
     return;