w3c / requestidlecallback

Cooperative Scheduling of Background Tasks
https://w3c.github.io/requestidlecallback/
Other
50 stars 19 forks source link

Clamp requestIdleCallback deadline #20

Closed esprehn closed 8 years ago

esprehn commented 9 years ago

Elliott: This still has the privacy problems where you can sample the time remaining with high accuracy. My issue about making the short idle periods all uniform in length hasn't been addressed.

Ross: You're right, sorry, this discussion fizzled out on the CL without a resolution that I was aware of. As I understand from Ilya's comment in https://codereview.chromium.org/1119683003/#msg64, your proposal was:

I am happy to add some clamping to the deadline reporting, however 2ms seems a bit too granular and would reduce the utility of rIC, particularly since the clamping of performance.now() is only to 5us. Is there a particular reason this API requires more aggressive clamping, considering that it probably wouldn't be much much more difficult to sample remaining frameTime using performance.now() and a combination of rAF + setTimeout(0)?

Eliott: No, we only ever report 2ms or > 20ms, but never anything in the middle. If your task will take longer than 2ms you should schedule another rIc and if there's enough time we call you back in the same frame.

rmcilroy commented 9 years ago

Regarding:

No, we only ever report 2ms or > 20ms, but never anything in the middle. If your task will take longer than 2ms you should schedule another rIc and if there's enough time we call you back in the same frame.

If we want to have an idle period being the time between frame commit and the start of drawing the next frame, then we would not be able to allow a rIC to schedule another callback in the same frame given this part of the spec:

Only idle tasks which were posted before the start of the current idle period <i.e., frame> are eligible to be run during the current idle period. This enables idle callbacks to re-post themselves to be run in a future idle period if they cannot complete their work by a given deadline."

We could possibly get around this by split each frame up into multiple 2ms idle periods, however this seems really limiting to the API. Apps which wanted to do something atomically, which takes more than 2ms but less than a frame (e.g., 16ms) would have to wait for a 20ms idle period, which might never happen if they have active animations on the page, no matter if there were actually time available between frames to do this work.

It's still not clear to me why we need this level of clamping. I don't see what this gains you over some form of granular clamping (e.g, clamping to 2ms and allowing 2ms, 4ms, 6ms, 8ms, etc.). Surely even if you were limited to 2ms deadlines but were allowed to repost a rIC to be run in the same frame you could work out the overall length of the frame based on the number of rIC callbacks which got run to the same level of accuracy as if the deadline was clamped granularly to 2ms?

esprehn commented 9 years ago

I don't agree with that part of the spec, it should not post you to the next idle period, it should continue in the current one until time is exhausted. There's space in the current frame, why wait? That's how microtask (MutationObserver, Promises) and other future callbacks will work. In the past this magic "schedule until next time" was also difficult for developers to use/reason about. They'll end up wrapping rIc in their own scheduler instead.

8ms without yielding to other tasks is also bad, it means you're starving other tasks on the main thread by taking up the full idle period. I don't think it's a good idea to ever monopolize the main thread idle time, each task should take a small amount of time and continue scheduling continuations. If your task will take 6ms you should break it up, otherwise there's a very high probability you'll cause jank (descheduling, GC, recompile, new dirtiness you caused). For example if your 2ms idle task schedules a big style recalc we should stop running idle tasks and run the recalc now while there's extra time instead of waiting until the frame boundary when we'd have to miss a frame.

Also note each task will run follow up microtasks (ex. Promise resolves, MutationObservers) which will take up more time, so you can't really use "all" the idle time without likely missing a frame.

igrigorik commented 9 years ago

Quoting @rmcilroy from a thread on public-webperf:

a requestIdleCallback registered during an idle period doesn't become eligible to be run until the next idle period. This allows code patterns like the following:

function checkSpelling(deadline) { if (performance.now() + 5 <= deadline.deadline) { // This will take more than 5ms so wait until we // get called back with a long enough deadline. requestIdleCallback(checkSpelling); return; } // do work... }

If this is called with a deadline of less than 5ms then it won't get run again until the next idle period, at which point the deadline may be large enough to do the work. If the same pattern was used with setImmediate then the callback would get to run again immediately with the same (or a slightly smaller) deadline each time until the deadline is finally used up, causing the CPU to burn power unnecessarily.


@esprehn it seems like there are two threads here, each at odds with each other:

  1. Original concern was that "you can sample the time remaining with high accuracy".
  2. Second concern is about reposting rIC callback into same idle block.

If we allow 2, then don't we also grant 1? As @rmcilroy noted, you can just continue requesting idle callbacks and add up the times to get an accurate estimate (and burn a lot of CPU in the process).

On the other hand, I do like your suggestion of scheduling smaller idle callbacks - i.e. don't advertise the entire idle block. That said, requiring it to be always 2ms doesn't seem right either... There are many cases where you can't reasonably expect to complete all your work in 2ms, or pause and resume your work in 2ms chunks. Further, with fixed 2ms + repost in same idle callback you can't even tell if going above that budget will cause you to drop a frame or you'll simply occupy idle time of next callback -- we need more resolution.

@rmcilroy somewhat related question: if there are multiple rIC callbacks scheduled and an idle block becomes available, how do we allocate time between them?

rmcilroy commented 8 years ago

I agree with igrigorik@ points above.

In relation to the question of how we run multiple rIC callbacks. We run callbacks in round-robin order, i.e., if a rIC callback reposts itself at the end of it's execution then it will be put at the end of the run-queue, and will get another chance to run after all the previously scheduled rIC callbacks have been run. When a new idle period starts, the top callback in the run-queue gets scheduled and gets a deadline of the expected end of the current idle period. If it uses all the time to the deadline then the next callback will have to wait for the next idle period, if not, then the next idle callback will get to run for the remaining time (and so on).

The reason for this model is so that idle callbacks get as large an allowance as possible, so that if they know that a given operation is likely to take, say 8ms, then they know when they would be able to perform that operation without causing jank. I agree that the a task running for 8ms without yielding to other tasks is bad if it would be possible to break that work up into smaller pieces, however this is not always possible (in fact many of the use-cases which prompted rIC were exactly these kinds of long atomic chunks of JS which caused the main thread to block for tens of ms).

For those cases where the tasks are more fine-grained it is possible to write a user-level scheduler which queues up fine-grained tasks and runs a number of the tasks during a given idle callback (in fact the Maps team have a prototype which does exactly this).

rmcilroy commented 8 years ago

@esprehn, during our meeting last week I believe we came to the conclusion that the idle callback's timeRemaining() method wasn't any more of a privacy concern than performance.now(). Would you be happy if we clamp this 5us like performance.now() for the clamping aspect of this discussion?

igrigorik commented 8 years ago

@rmcilroy @esprehn #23 is merged, are we good to close this?

esprehn commented 8 years ago

As discussed in person I think this is probably not much worse than setTimeout + requestAnimationFrame in the current scheduler architecture so this is probably fine in terms of clamping for now. We might want to follow up with the security team though, since setTimeout always clamps to 1ms I'm curious if requestIdleCallback (and maybe postMessage) could be used to precisely time very fast frames running.