Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timer order and timer throttling for hidden pages #5925

Open
shaseley opened this issue Sep 16, 2020 · 7 comments
Open

Timer order and timer throttling for hidden pages #5925

shaseley opened this issue Sep 16, 2020 · 7 comments

Comments

@shaseley
Copy link

Per the current spec (step 16 of the timer initialization steps), there’s an ordering guarantee such that when calling setTimeout(callback, timeout), callback will not run until other setTimeout callbacks with a timeout value <= |timeout| have run, for the same method context (and % nesting level adjustment in step 11).

For hidden pages, UAs often apply throttling to timers to save power, sometimes in ways that break this ordering guarantee.

My understanding of Safari’s approach is that they treat deeply nested timers differently, only throttling timers that have reached a certain nesting level, which breaks this ordering guarantee (i.e. new timers with equal timeout values can run before deeply nested timers).

Per MDN documentation of setTimeout, Firefox further treats timers in tracking scripts differently, applying additional throttling in background. I think this probably also technically breaks the ordering guarantee, although it seems safe to do so.

Chromium’s background timer throttling implementation so far has respected this ordering, but we are working on more aggressive timer throttling and would like to try Safari’s approach of treating deeply nested timers differently.

I’m wondering if we should update the spec to relax this ordering guarantee to match what UAs are doing? I think providing a guarantee within the same callback (stack) makes sense, but I'm not so sure about across async hops. For example:

An ordering guarantee here makes sense, even for background pages.

function example1() {
  setTimeout(() => { console.log('This should print first.'); }, 100);
  setTimeout(() => { console.log('This should print second.'); }, 100);
  setTimeout(() => { console.log('This should print third.'); }, 100);
}

But what about here?

window.addEventListener('message', () => {
  setTimeout(() => { console.log('timer from postMessage') }, 100);
});

function deeplyNestedTimeout() {
  // This might be throttled in the background. Should it be guaranteed to run before any
  // setTimeout(100)s called after?
  setTimeout(() => { console.log('Deeply nested timer') }, 100);

  // Trigger a setTimeout with an async hop which might not be throttled.
 postMessage(null, '*'); 
}

Thoughts?

@domenic
Copy link
Member

domenic commented Sep 16, 2020

This seems like a reasonable loosening to me, if it's indeed the route browsers are going down. @rniwa @annevk, any thoughts?

I think providing a guarantee within the same callback (stack) makes sense, but I'm not so sure about across async hops. For example:

Probably the spec way to do that is within the same task. Since the sentence you mention is already pretty high-level, we could just expand it to

Wait until any invocations of this algorithm that had the same method context, happened during the same task, that started before this one, and whose timeout is equal to or less than this one's, have completed.

(or some variant of that which makes the now-extra-long sentence a bit more readable.)

@annevk
Copy link
Member

annevk commented Sep 17, 2020

I'm not sure how Firefox's approach breaks the ordering guarantees and from OP it's not directly clear to me why it would be desirable to do so.

cc @farre

@shaseley
Copy link
Author

Regarding Firefox and tracking/analytics scripts, the MDN documentation mentions that for tracking scripts, "In background tabs, however, the throttling minimum delay is 10,000 ms, or 10 seconds, which comes into effect 30 seconds after a document has first loaded."

This suggests to me that the timeout values for certain timers (coming from certain scripts) can be increased more than others coming from different scripts. But ordering guarantees are for method context (document or worker), not on a per-script basis, so it seems like this can potentially violate the spec ordering.

For example, suppose > 30 s after page load the following sequence occurs, in short succession:

  1. setTimeout(trackingCallback, 100) // called from tracking script
  2. setTimeout(nonTrackingCallback, 100) // called from non-tracking script, same document

IIUC per spec, if (1) happens before (2), trackingCallback must run before nonTrackingCallback.

But if these act like

  1. setTimeout(trackingCallback, 10000) // tracking script min bg timeout
  2. setTimeout(nonTrackingCallback, 1000) // non-tracking script min bg timeout (default)

then the opposite order seems possible.

I’m not sure how this is implemented in FF (or if this is up-to-date), so insight here would be great! I also don’t think this is likely observable or problematic, but I am wondering if it’s technically a spec violation, and if so, should the spec be updated.

As to why we’re (Chromium) interested in this, we’d like to throttle based on nesting level (similar to Safari), applying further throttling to deeply nested timers. We think that deeply nested timers are the most problematic in terms of power consumption and differentiating strikes a good balance between saving power and not breaking certain use cases (see also blink-dev discussion).

It’s still possible to differentiate based on nesting level and preserve ordering, but would require special handling for detecting ordering violations and running any throttled deeply nested callbacks whose timeout is <= the non-nested timer first. We feel that simply differentiating them and scoping the ordering guarantee to a task will:

  1. Be easier to explain to developers
  2. Increase compat w/ Safari’s throttling
  3. Simplify the implementation

@farre
Copy link
Contributor

farre commented Sep 18, 2020

So I looked through our implementation, and at one point we had a setTimeout where if it was called from a script on our tracking protection list, it would potentially be executed out of order. We backed that out, because it broke too much stuff, and we instead opted to go with a more general throttling strategy. Ordering seemed to be more important that we thought.

So Firefox does respect ordering, when the method context is the same, although MDN says otherwise.

@shaseley
Copy link
Author

@farre Thanks a lot for the info and looking at the implementation. Were you able to determine if the breakages were due to the longer delays/extra throttling, or if it was because of ordering?

@rniwa any thoughts from the WebKit side, and do you have a sense of how much broke as a result of WebKit's additional throttling of nested timers? TIA.

@farre
Copy link
Contributor

farre commented Sep 24, 2020

We experimented a lot with grace periods before throttling would start at all, but problems still remained so we definitely got the impression that order was an issue.

@othermaciej
Copy link

It seems like a good idea to update the spec to allow what implementations do in practice. I'm not an expert on our timer code and from a quick scan, I can't tell if it would violate the ordering guarantees, but the code definitely does track and care about nesting level of non-repeating timers. Besides @rniwa it's possible @cdumez would know what's up here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

5 participants