Last Comment Bug 647001 - Timeouts clamped in background tabs don't immediately unclamp when switching to the tab
: Timeouts clamped in background tabs don't immediately unclamp when switching ...
Product: Core
Classification: Components
Component: DOM (show other bugs)
: Trunk
: All All
: -- normal (vote)
: mozilla7
Assigned To: Emanuele Costa
Depends on: 669158
Blocks: 633421 663020
  Show dependency treegraph
Reported: 2011-03-31 14:52 PDT by Johnny Stenback (:jst,
Modified: 2011-08-19 04:29 PDT (History)
9 users (show)
bzbarsky: in‑testsuite?
See Also:
Crash Signature:
QA Whiteboard:
Iteration: ---
Points: ---
Has Regression Range: ---
Has STR: ---

nsGlobalWindow patch (4.56 KB, patch)
2011-06-20 07:03 PDT, Emanuele Costa
no flags Details | Diff | Splinter Review
nsGlobalWindow patch (4.63 KB, patch)
2011-06-21 06:42 PDT, Emanuele Costa
bzbarsky: review-
Details | Diff | Splinter Review
nsGlobalWindow patch (5.00 KB, patch)
2011-06-23 01:22 PDT, Emanuele Costa
no flags Details | Diff | Splinter Review
nsGlobalWindow patch (9.11 KB, patch)
2011-06-28 07:46 PDT, Emanuele Costa
bzbarsky: review-
Details | Diff | Splinter Review
nsGlobalWindow patch (9.65 KB, patch)
2011-06-28 14:46 PDT, Emanuele Costa
bzbarsky: review-
Details | Diff | Splinter Review
nsGlobalWindow patch (10.42 KB, patch)
2011-06-28 15:58 PDT, Emanuele Costa
bzbarsky: review+
christian: approval‑mozilla‑aurora-
Details | Diff | Splinter Review

Description Johnny Stenback (:jst, 2011-03-31 14:52:47 PDT
Not sure how much we care here, but this can certainly be observed by users. To reproduce (stolen largely from bug 646972):

1)  Make sure you have new tabs set to open in background.
2)  Open the context menu for the "URL" field link on this bug.
3)  Select "open in new tab"
4)  Wait 10 seconds.
5)  Switch to the new tab.
6)  Observer when the timeouts start firing every 10ms or so.

What you can see, depending on where in a given second you happen to switch to a tab, is that you switch, it looks like the page is stuck for a short while (anywhere between 0 and 1 seconds), then it wakes up.

I'd imagine users could notice this on pages that animate with fairly high frequency, and the effect would be that it appears Firefox isn't animating when they switch to the tab, but then all of a sudden it comes to life.

We could fix this by making the call to nsGlobalWindow::SetIsBackground(PRBool aIsBackground) is called with aIsBackground set to false, and restarting clamped timers at their unclamped interval at that point, or somesuch.

Comment 1 Robert O'Callahan (:roc) (email my personal email if necessary) 2011-03-31 15:17:13 PDT
I think we should probably fix this.
Comment 2 Boris Zbarsky [:bz] (TPAC) 2011-03-31 22:11:06 PDT
I did think about this when fixing bug 646972.  I'll see what I can do.  Just reiniting the timers can work, if done carefully.
Comment 3 Boris Zbarsky [:bz] (TPAC) 2011-05-31 21:32:56 PDT
Emanuele wants to give this a shot.
Comment 4 Emanuele Costa 2011-06-20 07:03:53 PDT
Created attachment 540452 [details] [diff] [review]
nsGlobalWindow patch

I virtualized SetIsBackground and created a function that will cycle and re-init all the non cleared timers with the correct DOMMinTimeoutValue interval.
Comment 5 Emanuele Costa 2011-06-20 19:51:20 PDT
also notice for the time being the for-loop may raise an error but the nsresult can be overwritten depending on the NS_ERROR behaviour. I am not sure what would it be better to do in case of  InitWithFuncCallback failure. To keep looping or return?
Comment 6 Emanuele Costa 2011-06-21 06:42:47 PDT
Created attachment 540728 [details] [diff] [review]
nsGlobalWindow patch

fixed typo
Comment 7 Boris Zbarsky [:bz] (TPAC) 2011-06-21 07:25:03 PDT
Comment on attachment 540728 [details] [diff] [review]
nsGlobalWindow patch

A few things wrong here:

1)  Most importantly, resetting for 

   timeout->mWhen = TimeStamp::Now() + nextInterval;

is wrong.  It will fire the timer too late.  Worse yet, it will increase the firing time each time SetIsBackground() is called, and does this for all timeouts, so long-running timers might well never fire with this setup (consider a timer set for 5 minutes, with the user switching away from the tab and then back every 3 minutes.

2)  Calling the method ResetTimeoutOrInterval is not so great.  The point of the method is to deal with resetting timers when going active, really.  So I would only call it when aIsBackground is false while mIsBackground is true.  And I'd call it something like UnclampTimeouts or ResetNonbackgroundTimeouts.

What I think you want to do in that method is to stop walking once you have passed all the timers that were clamped.  For each timer that was clamped you need to reset it for when it would have fired if the old clamp were not applied and the new one is applied instead.

Note that mInterval is 0 for non-repeating timeouts at the moment, so your code doesn't work correctly for those...  I bet with your patch a 5 minute setTimeout fires almost immediately if you switch away from the tab and then back.
Comment 8 Emanuele Costa 2011-06-23 01:22:36 PDT
Created attachment 541307 [details] [diff] [review]
nsGlobalWindow patch

Ok, this should be a step closer.

I tried all combinations of timeouts including switching tabs very quickly after setting a long timeout of 5secs:

  var lastTime = new Date();
  function f() {
    var now = new Date();
    document.body.appendChild(document.createTextNode((now - lastTime) + " "));
    lastTime = now;
  setInterval(f, 5000)

Last thing that should be left doing is breaking the loop after unclamp timers are all processed.
Comment 9 Boris Zbarsky [:bz] (TPAC) 2011-06-23 15:54:36 PDT
timeout->mInterval is going to be 0 for non-repeating timers... so as far as I can tell, this will make those fire too soon.  Test something with a setTimeout(f, 3600000), for example.....
Comment 10 Emanuele Costa 2011-06-28 07:46:46 PDT
Created attachment 542467 [details] [diff] [review]
nsGlobalWindow patch

Some testing code for future reference, to test intervals:

  var lastTime = new Date();
  function f() {
    var now = new Date();
    document.body.appendChild(document.createTextNode((now - lastTime) + " "));
    lastTime = now;
  setInterval(f, 0)

to test timeouts (pay attention to #define DOM_CLAMP_TIMEOUT_NESTING_LEVEL):


var lastTime = new Date();
function f() {
    var now = new Date();
    document.body.appendChild(document.createTextNode((now - lastTime) + " "));
    lastTime = now;
  setTimeout(f, 0)

document.write("<p>Starting timers.</p>");


after DOM_CLAMP_TIMEOUT_NESTING_LEVEL they should start behaving like intervals.

I also took away the clumsy mInterval = 0 logic and replaced with a proper boolean mIsInterval in the corresponding if conditions.
Comment 11 Emanuele Costa 2011-06-28 07:48:15 PDT
Comment on attachment 542467 [details] [diff] [review]
nsGlobalWindow patch

Review of attachment 542467 [details] [diff] [review]:
Comment 12 Boris Zbarsky [:bz] (TPAC) 2011-06-28 09:05:53 PDT
Comment on attachment 542467 [details] [diff] [review]
nsGlobalWindow patch

>+void nsGlobalWindow::SetIsBackground(PRBool aIsBackground)
>+  nsPIDOMWindow::SetIsBackground(aIsBackground);
>+  if(PR_FALSE == aIsBackground)
>+    ResetNonbackgroundTimers();

I would prefer that you do something like:

  PRBool resetTimers = (!aIsBackground && IsBackground());
  if (resetTimers) {

>@@ -8869,16 +8877,17 @@ nsGlobalWindow::SetTimeoutOrInterval(nsI
>   if (aIsInterval) {
>     timeout->mInterval = interval;
>   }

That set should be unconditional.

>+nsresult nsGlobalWindow::ResetNonbackgroundTimers()

I think this may be better named "ResetTimersForNonBackgroundWindow".

>+  nsTimeout *timeout;
>+  TimeDuration nextInterval = 0;
>+  TimeDuration previousInterval = 0;

These should all be declared in the for loop (|timeout| in the loop header, and the rest when you actually go to use them.

>+  TimeStamp now;

And this should either be declared in the loop or initialized here, not every time through the loop.

>+  for (timeout = FirstTimeout(); IsTimeout(timeout); timeout = timeout->Next()) {
>+    if (timeout->mIsInterval || timeout->mNestingLevel >= DOM_CLAMP_TIMEOUT_NESTING_LEVEL) {

You only need this test to check whether the timer was clamped to start with, right?  But that's not the right check, since clamping also depends on the timer's mInterval.  I'd just take this check out altogether.

You might also want to stop the loop once you get to a timer for which |mWhen - now > TimeDuration::FromMilliseconds(gMinBackgroundTimeoutValue)|, since anything after that point clearly doesn't need resetting (timers are stored in mWhen order, and anything that far in the future didn't get clamped to start with).

>+      /* We switched from/to background, re-init the timeout/interval appropriately */

We know we switched _from_ background here.

>+      // Compute time to next timeout for interval timer
>+      // using DOMMinTimeoutValue().

It might not be an interval timer.  How about:

  // Compute the interval the timer should have had if it had not been set in a background window

>+      nextInterval = TimeDuration::FromMilliseconds(NS_MAX(timeout->mInterval,
>+                                                         PRUint32(DOMMinTimeoutValue())));

Fix the indentation here.  And maybe s/nextInterval/interval/?

>+        // unclamp
>+        timeout->mWhen = timeout->mWhen - TimeDuration::FromMilliseconds(gMinBackgroundTimeoutValue);
>+        rv = timeout->mTimer->InitWithFuncCallback(TimerCallback, timeout,
>+                                                   nextInterval.ToMilliseconds(),
>+                                                   nsITimer::TYPE_ONE_SHOT);

This looks wrong.  Say timeout->mInterval is 10s and we set the timer up 5s ago.  Then nextInterval will be 10s, timeout->mWhen will be "now + 5s".  This code will reset timeout->mWhen to "now + 4s" and then set a 10s timer.   So the timeout will fire after 15s, not 10s

I think the right thing to do here is to compute |interval| as above, then get the delay from the nsTimeout's timer (call that variable |oldDelay|).  Now you know that the timer was set up at timeout->mWhen - oldDelay, and hence its new firing time should be timeout->mWhen - oldDelay + nextInterval.  This is what timeout->mWhen should be reset to, and the delay passed to InitWithFuncCallback should then be the new timeout->mWhen - now.  Note that this means you don't need the mWhen-now check either (which is good, because that check is wrong; just because mWhen is close to now doesn't mean the timer's firing time needs changing; it could be a 10s timer set up 9.997s ago).

The other thing you will need to do when changing mWhen values is to make sure to move the nsTimeout to the right place in the list so it remains sorted in mWhen order.

>+++ b/dom/base/nsGlobalWindow.h
>   // Non-zero interval in milliseconds if repetitive timeout
>   PRUint32 mInterval;

That comment needs fixing.
Comment 13 Emanuele Costa 2011-06-28 14:46:08 PDT
Created attachment 542610 [details] [diff] [review]
nsGlobalWindow patch

revised logic.
Comment 14 Boris Zbarsky [:bz] (TPAC) 2011-06-28 14:56:19 PDT
Comment on attachment 542610 [details] [diff] [review]
nsGlobalWindow patch

>+++ b/dom/base/nsGlobalWindow.cpp
>+    if (timeout->mWhen - now > TimeDuration::FromMilliseconds(gMinBackgroundTimeoutValue))
>+      break;

This could use curly braces and a comment explaining why it's safe to break.

>+    if (TimeDuration::FromMilliseconds(oldInterval) > interval) {

Save TimeDuration::FromMilliseconds(oldInterval) in a temporary, instead of computing it twice?

>+      PR_REMOVE_LINK(timeout);
>+      InsertTimeoutIntoList(timeout);

These should come before the attempt to reinit the timer and ensuing early return on failure.

Also, you nee to do the |timeout = timeout->Next()| bit _before_ doing this, no?  And add a comment that says that since mWhen is strictly smaller now (something we should enforce by only unclamping if mWhen > now to start with), it's safe to do the reinsertion because it won't affect our list traversal.

>   // Non-zero interval in milliseconds if repetitive timeout
>   PRUint32 mInterval;

This comment still needs fixing.

The rest looks good!
Comment 15 Emanuele Costa 2011-06-28 15:58:18 PDT
Created attachment 542642 [details] [diff] [review]
nsGlobalWindow patch

added linked list proper handling and extra break logic in the loop
Comment 16 Boris Zbarsky [:bz] (TPAC) 2011-06-28 21:40:55 PDT
Comment on attachment 542642 [details] [diff] [review]
nsGlobalWindow patch

>+ nsresult rv = NS_OK;

Declare this where it's used; converting the break on failure into a return makes that possible.

>+  nsTimeout *timeout = nsnull;

Again, that can go in the for loop header.

>+    TimeDuration interval = 0;
>+    PRUint32 oldIntervalMillisecs = 0;

Declare these where they're used, not up front?

>  for (nsTimeout *timeout = FirstTimeout(); IsTimeout(timeout); timeout = timeout->Next()) {

You don't want to do the timeout = timeout->Next() bit here, since you already advanced through the list.

>-    if (timeout->mWhen < now)

This should be <=, I think, so you can guarantee that after unclamping timeout->mWhen is _strictly_ less than the old mWhen.  Since it might equal |now| at that point, have to use <= here.

>+    if ((timeout->mWhen - now > TimeDuration::FromMilliseconds(gMinBackgroundTimeoutValue))
>+        || IsFrozen() || mTimeoutsSuspendDepth) {

The IsFrozen() || mTimeoutsSuspendDepth check should just come up front in this function.

r=me with the above fixed.  Thank you!

I'm going to make the above changes and a few comment changes that I think explain better what we're doing and push the result to try.
Comment 17 Boris Zbarsky [:bz] (TPAC) 2011-06-28 22:08:58 PDT
Comment 18 Boris Zbarsky [:bz] (TPAC) 2011-06-28 22:11:24 PDT
Er, that was clearly buggy; removing the |timeout = timeout->Next()| means you have to manually do it when not unclamping.
Comment 19 Boris Zbarsky [:bz] (TPAC) 2011-06-28 22:18:47 PDT
Oh, and one other thing I found while testing this: have to call Release() on the timeout after InsertTimeoutIntoList().  And reset its mFiringDepth.
Comment 20 Boris Zbarsky [:bz] (TPAC) 2011-06-28 22:38:42 PDT
The patch does fix bug 663020.

Pushed again to try as
Comment 21 Boris Zbarsky [:bz] (TPAC) 2011-06-29 07:03:49 PDT
That failed a bunch of tests because we built a place that treated mInterval as boolean: clearInterval was setting it to 0 to signal that a currently-running interval should be canceled and its timer not reset.

I changed that code to set mIsInterval instead, which fixes the failures I spot-checked locally, and pushed to try again:
Comment 22 Boris Zbarsky [:bz] (TPAC) 2011-06-29 14:10:05 PDT
Rollup patch pushed as
Comment 23 Boris Zbarsky [:bz] (TPAC) 2011-06-29 14:22:12 PDT
Comment on attachment 542642 [details] [diff] [review]
nsGlobalWindow patch

We should probably consider taking this on Aurora.  This fixes a behavior regression on at least one site.
Comment 24 Marco Bonardo [::mak] 2011-06-30 05:51:36 PDT
Comment 25 christian 2011-06-30 14:30:30 PDT
Comment on attachment 542642 [details] [diff] [review]
nsGlobalWindow patch

We decided this can wait for Firefox 7...denied for mozilla-aurora
Comment 26 Trif Andrei-Alin[:AlinT] 2011-08-19 02:29:43 PDT
Mozilla/5.0 (Windows NT 6.1; rv:7.0) Gecko/20100101 Firefox/7.0

Could you give some specific steps how can i check if it was fixed?
I've followed the steps in comment 1,and i've noticed a little time out when switching tabs.
Comment 27 Trif Andrei-Alin[:AlinT] 2011-08-19 04:29:39 PDT
Thanks for the enlightenment E.
Setting resoution to VERIFIED FIXED.

Note You need to log in before you can comment on or make changes to this bug.