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

Proposal: self.queueMicrotask(f) #512

Closed
domenic opened this issue Jan 13, 2016 · 46 comments
Closed

Proposal: self.queueMicrotask(f) #512

domenic opened this issue Jan 13, 2016 · 46 comments
Labels
addition/proposal New features or enhancements

Comments

@domenic
Copy link
Member

domenic commented Jan 13, 2016

In talking with the Angular team (mostly @mhevery), there is interest in a simple low-level method for enqueuing a microtask from author code. I've heard this from a number of authors in the past. It seems simple enough and easy to add to the spec, if we can get any other implementations besides Chrome interested.

Right now authors are doing this via a number of hacks, like creating a text node and a MutationObserver and twiddling the text node every time they want to trigger a microtask, or using Promise.resolve().then(f) (which creates a promise object and stores any thrown errors there, instead of letting them propagate to "report an exception").

There are many use cases, which I'm sure we can assemble in depth, but let me just start with the following reasons which will hopefully be enough to convince people:

  • From an academic point of view, this is a low-level primitive and there's no reason to lock it up behind higher-level concepts like promises and mutation observers.
  • From a practical point of view, libraries very often want to guarantee asynchrony (to avoid Zalgo), without taking the overhead of a full "macro" task like setTimeout(f, 0).

I'd like to first focus this thread on finding implementer support: if we spec this, will you implement it? After that, we can start bikeshedding the name; please don't do so beforehand.

/ccing some implementers: @hober @travisleithead @bzbarsky

@domenic domenic added the addition/proposal New features or enhancements label Jan 13, 2016
@bzbarsky
Copy link
Contributor

This makes sense as a primitive to have. The only concern is what we do once it starts getting abused (see setTimeout throttling). Of course we'd have to figure that out for Promise and MutationObserver too....

@bzbarsky
Copy link
Contributor

I would also suggest posting to the Mozilla dev-platform mailing list to get a wider set of Mozilla people looking at this. I can do that for you if you want; please let me know.

@domenic
Copy link
Member Author

domenic commented Jan 13, 2016

That'd be lovely if you could; thank you.

@bzbarsky
Copy link
Contributor

@ianb
Copy link

ianb commented Jan 13, 2016

What is the difference between window.enqueueMicrotask(f) and window.setTimeout(f) ?

@khuey
Copy link

khuey commented Jan 13, 2016

setTimeout returns to the event loop and processes events.

@domenic
Copy link
Member Author

domenic commented Jan 13, 2016

For more information on that question I'd suggest reading the spec which has detailed sections on setTimeout and on enqueuing a microtask.

@smaug----
Copy link

Abusing is an issue, and yes, we may need to do something to that with MutationObserver and
Promise case too. (I can see long Promise chains to be rather horrible from responsiveness point of view.) Abuse is here even more an issue than with setTimeout, since microtask is kind of an async concept, but only from js execution side, not from event loop.

We should think about abusing issues before we add the API. But in general sounds like a good idea.

@domenic
Copy link
Member Author

domenic commented Jan 14, 2016

I think abuse is taken care of by the slow script dialog clause already: https://html.spec.whatwg.org/#killing-scripts

@smaug----
Copy link

I'm not so sure.
We're giving a new model for running JS almost-async. That 'almost' can be hard to understand. (at least based on my experience - I've had to explain microtasks quite a few times).

Initially microtasks were for one particular case only - running MutationObserver callbacks, and in that case abusing is less likely. Promises make abusing way more likely, and raw microtask callbacks perhaps even more.

But it is not clear to me what we could do to prevent abuse.

@domenic
Copy link
Member Author

domenic commented Jan 14, 2016

Well, whatever you want to do, I imagine it will fall under one of

the user agent may either throw a QuotaExceededError exception, abort the script without an exception, prompt the user, or throttle script execution.

so I still think it's covered by the existing spec.

@smaug----
Copy link

Except that if we add some limitation, it needs to work consistently across browsers.

@domenic
Copy link
Member Author

domenic commented Jan 14, 2016

Why? The slow script dialog doesn't work consistently across browsers today. Recursive microtasks are essentially the same as recursive functions: it allows an extra stack frame to unwind in between, but there's otherwise no difference. So I think they should be treated the same way.

@smaug----
Copy link

ok, so you're worried about blocking UI totally with effectively while(true);
I'm also worried about moving to a model where people start to use more
window.enqueueMicrotask and less window.setTimeout..
Having several nested microtasks may easily end up pushing the next rAF callback time to be
too late. setTimeout has less that problem since it is truly async, and may just happen after rAF if needed.

@domenic
Copy link
Member Author

domenic commented Jan 14, 2016

I don't think enqueueMicrotask is really an alternative to setTimeout. It's specifically used when people want to do work before rendering. In other words, it's for choosing between

function f(cb) {
  if (someTest) cb();
  else {
    doAsyncThings().then(cb);
  }
}

(which is bad since it's sometimes-sync, sometimes-async) versus

function f(cb) {
  if (someTest) enqueueMicrotask(cb);
  else {
    doAsyncThings().then(cb);
  }
}

People can abuse it by not yielding to the event loop to allow rendering/RAF, for sure. But they can also do that with while(true). If you want to yield to the event loop, you don't use enqueueMicrotask.

@mhevery
Copy link

mhevery commented Jan 14, 2016

I would like to offer my point of view as the Angular author. enqueueMicrotask, setTimeout and requestAnimationFrame fill very different need.

enqueueMicrotask is for, I need to do something outside my current frame, but before the rendering happens. If I use setTimeout I will have flicker, since the renderer will draw intermediate view.

setTimeout is for when I am doing long work, and wish to break it up into smaller turns to allow browser so breathing room.

requestAnimationFrame is well, for animation.

So these things all fill a different niche. Yes, all of them can be misused, but let's not that be enemy of useful.

@phistuck
Copy link

Why is this not proposed as a ECMAScript feature?

@annevk
Copy link
Member

annevk commented Jan 18, 2016

ECMAScript doesn't have a good concept of the event loop.

@jakearchibald
Copy link
Contributor

For those looking for an explanation of tasks (eg setTimeout) and microtasks https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

@phistuck
Copy link

@annevk - however, Promise already uses microtasks... It would be very weird to have a primitive on which the language depends, be defined separately, in a web specific context.

@smaug----
Copy link

@mhevery still trying to understand the use cases here. If you want to do something before rendering, you could just use rAF. Why does that not work for you?

What you mean with "need to do something outside my current frame"? What is 'frame' here?

Random thought. it feels like people often want setTimeout(,0) but with guarantee it is called before the next rAF. So, true asynchronousness, but prioritized over rAF. If such scheduling type was added, the method could even throw or return false if we're about to delay rAF because of it.
Such setup would let UA to process user input between the callback calls, unlike microtask callbacks.

(I'm just trying to understand both pros and cons and also use cases for this. In general I'm in favor of adding this API, but we're too good at adding APIs to the platform and realizing later that it wasn't quite right. )

@calvinmetcalf
Copy link

I suspect that the lack of public microtask api is why a lot of frameworks have their own next tick method, see

@mhevery
Copy link

mhevery commented Jan 18, 2016

@SmauG setTimeout and rAF can cause UI flicker (rendering partially rendered UI), and as such are not acceptable for the frameworks, as a result as @calvinmetcalf points out many frameworks have their own microtask queues. The issue is that we now have two microtask queue, one for framework and one for Promises, which mean that that the two have different priorities. Frameworks often need to schedule 100s of micro tasks, using setTimeout and rAF just does not work, since it is to slow and causes flicker.

Yes Promises is a way to schedule microtasks, but in my opinion it is backwards. The primitive is enqueueMicrotask not Promise. Promise can be implemented using enqueueMicrotask. (The opposite is also true, one can implement enqueueMicrotask using Promises but that just seems weird.) If you try to polyfil enqueueMicrotask you will see that the polyfill is way smaller then a polyfill for Promise, which is why I think that enqueueMicrotask is the lower primitive.

@smaug----
Copy link

the whole point of rAF is to not flicker, but do async stuff as rarely as possible right before rendering is updated. Sure, if you request a new animation frame inside rAF, then that gets called later.

Hmm, 100s of microtasks. That certainly sounds like abuse of microtasks. Doing too much stuff synchronously (remember, microtask is synchronous from browser point of view).
We've tried to move to use more async APIs in the platform and now we're proposing a new synchronous API which would be possibly heavily used and has somewhat high risk of affecting
badly to responsiveness.

So, still wondering. Is there some other kind of, truly asynchronous API which frameworks could use?

@annevk
Copy link
Member

annevk commented Jan 18, 2016

@smaug---- if frameworks are already doing this today presumably they have a reason for that. And other than a microtask API there's really nothing else that'd guarantee the thing happening after the current activity, but before the next task (except for the hacks frameworks are already using).

@domenic
Copy link
Member Author

domenic commented Jan 18, 2016

@phistuck

however, Promise already uses microtasks... It would be very weird to have a primitive on which the language depends, be defined separately, in a web specific context.

That's not true. Promises use a host-environment agnostic concept called "jobs". When hosted by a user agent implementing the HTML Standard, jobs map to the HTML concept of microtasks. See https://html.spec.whatwg.org/#integration-with-the-javascript-job-queue.

This is clearly out of scope for ECMAScript, and the committee has (often violently) pushed back against the very word "microtasks" when it is brought up in TC39 meetings.

@domenic
Copy link
Member Author

domenic commented Jan 18, 2016

Hmm, 100s of microtasks. That certainly sounds like abuse of microtasks. Doing too much stuff synchronously (remember, microtask is synchronous from browser point of view).

What are you talking about? I have way over 100 lines of code that run synchronously. Sometimes I want them to run a specific order, so I use microtasks to ensure that ordering. Other times I just let them run in the source order, but you don't call that an "abuse."

@annevk
Copy link
Member

annevk commented Jan 18, 2016

I talked to @smaug---- on IRC. His issue is that user input only comes in after a tasks completes (in a new task). So adding a mechanism to queue a lot of "event-loop blocking" work is a concern. His suggestion is to have an API that ensures the callback is invoked before next-rAF. This would not block going to the next task, but would block painting.

Since you can already have a microtask API through a polyfill, perhaps we should have both.

@domenic
Copy link
Member Author

domenic commented Jan 18, 2016

That seems like a reasonable separate feature request in a separate GitHub issue. I am curious if any framework authors are interested in that, or just Gecko.

@mhevery
Copy link

mhevery commented Jan 19, 2016

User event comes in, framework starts processing it. In the process there
may be a lot of Promise.then sequences. These things can evaluate truly
async (as in long stretches of user wait, waiting for XHR), or they may
execute in microtask async because the data is cached. These Promise.then
chains can easily create 100s of microtasks on a reasonably sized
application. The framework wants to guarantee, that it will not render
until all microtasks are processed. (since rendering too early would be
wasted effort, as the work would be invalidated by later microtasks). If
the framework schedules rAF there is no guarantee, that browser will not
insert a render frame in between the microtasks and rAF, which would render
intermediate results (flicker to the user). To complicated the matter
further, sometimes applications want to do DOM measurements
(width/height/position), which can be only done after the framework
renders. After these measurements, the app often adjusts some positions,
which requires incremental update to the DOM.

Yes we are probably busting our frame budget, but I don't don't believe
that forcing intermediate/wrong/flicker frames is what the app developer
wants. Everyone wants 60 fps, but everyone wants the correct frames first,
before high frame rate.

On Tue, Jan 19, 2016 at 7:01 AM smaug---- [email protected] wrote:

If frameworks call hundreds of callbacks, doing all that during rAF time
might put too much pressure to the rAF and effectively rendering would get
delayed.
(But still don't know why that would cause flickering)


Reply to this email directly or view it on GitHub
#512 (comment).

@bzbarsky
Copy link
Contributor

@mhevery Thank you for the example.

Just to make sure I understand it properly, is the framework trying to wait until the entire Promise chain completes, even if in the async case, or is it just trying to wait until all the things that can be serviced out of cached data are done before rendering? I assume the latter, right?

there is no guarantee, that browser will not insert a render frame in between the microtasks and rAF

The whole point of rAF is that there is such a guarantee: if you make some changes to the DOM or styles and in the same task or ensuing microtasks post an rAF callback, then that rAF callback will be called before the browser renders to screen any of the DOM/style changes you made.

sometimes applications want to do DOM measurements (width/height/position), which can be only done after the framework renders.

Again, just to make sure I understand correctly, the use case here is that the framework wants to render once all the cached stuff has been applied, without necessarily waiting for the truly async tasks, and the framework consumer wants to do some work after the framework renders but before the browser then renders the output of the framework. Is that a correct summary of the problem?

@mhevery
Copy link

mhevery commented Jan 19, 2016

@bzbarsky

Just to make sure I understand it properly, is the framework trying to wait until the entire Promise chain completes, even if in the async case, or is it just trying to wait until all the things that can be serviced out of cached data are done before rendering? I assume the latter, right?

Yes, wait until everything that can be serviced is.

The whole point of rAF is that there is such a guarantee:

OK, I stand corrected, did not realized that. But if we use rAF while there is no flicker, now we are running the risk of not rendering, and showing the wrong state to an intermediate. For example let's assume that user clicks, and frameworks schedules a rAF to render. then setTimeout fires (before rAF) The issue is that setTimeout will incorrectly see a half processed state of click. Rendering is not just updating the DOM, but also propagating the data through the system, and since we did not propagate the data, setTimeout will see unfinished click work, which would create really hard to track down issues. You can think of the event and the subsequent rendering as a transaction. Nothing can be in between.

What we need is a way to schedule work on the current microtask queue. rAF is too late since there could be other VM turns in between.

Again, just to make sure I understand correctly, the use case here is that the framework wants to render once all the cached stuff has been applied, without necessarily waiting for the truly async tasks, and the framework consumer wants to do some work after the framework renders but before the browser then renders the output of the framework. Is that a correct summary of the problem?

Yes correct. Just adding, that I keep using render, but it really should be data propagation. The fact that some data propagates to the DOM and other propagates to the other part of the application is important. While DOM propagation could be delayed, (since there is no side-effect) propagating to the rest of the app can not be delayed, since we need to treat it as transaction, and fully complete it before we allow subsequent event processing.

@bzbarsky
Copy link
Contributor

setTimeout will see unfinished click work

Thank you, I appreciate this example.

That said, even if the framework were to append to the current microtask queue, that just pushes the problem back a level: now the setTimeout example will see the framework render completed, but a Promise callback or mutation observer might not; this is vaguely reminiscent of the "I want my app to always be on top" problem. That said, this would only apply to Promise callbacks for Promises created during the framework's handling of click, I think, and similarly only for mutation observers for mutations triggered by the framework, so maybe it's not a problem in practice...

@mhevery
Copy link

mhevery commented Jan 19, 2016

Promise callback or mutation observer might not

But we don't guarantee that. We only guarantee that VM turns will see it consistent.

I think, and similarly only for mutation observers for mutations triggered by the framework, so maybe it's not a problem in practice...

correct, since every VM turn is started with an event (click or timer setTimeout), Promises are intermediate items, they never start a VM turn.

@saschanaz
Copy link
Member

saschanaz commented Apr 30, 2017

I think there was a similar try with setImmediate, is there any major reason to make a new spec?

@jakearchibald
Copy link
Contributor

setImmediate queues a task, not a microtask.

domenic added a commit that referenced this issue Jun 23, 2017
@domenic domenic changed the title Proposal: window.enqueueMicrotask(f) Proposal: self.queueMicrotask(f) Jul 9, 2018
@domenic
Copy link
Member Author

domenic commented Jul 9, 2018

Resurrecting this old thread:

Chrome is currently implementing the proposal as specced in #2789. Are any other vendors interested in shipping queueMicrotask()? Lots of discussion with Gecko folks above; what about Safari (@cdumez @hober) or Edge (@dstorey @travisleithead)?

Note how #2789 has web developer-facing text about when this is appropriate which should help at least a bit with some of the Gecko discussions here.

domenic added a commit that referenced this issue Jul 9, 2018
@travisleithead
Copy link
Member

@dstorey, @arronei and I are glad to see the Author note. I'm a little concerned about developers easily falling into the trap of scheduling the next callback from within the callback (e.g., like rAF) that will lead to accidental infinite callbacks without a render/paint. But there's no solution I can think of to prevent that while still enabling the scenario :-). No objections here.

@domenic
Copy link
Member Author

domenic commented Jul 10, 2018

Thanks @travisleithead and folks! We could indeed add some examples drawing explicit equivalence between recursive queueMicrotask and recursive sync functions / sync while loops.

I'm a little unsure how we should interpret "No objections here" for the purposes of https://whatwg.org/working-mode#additions; would you support adding this to the specification?

@dstorey
Copy link
Member

dstorey commented Jul 16, 2018

Yes

domenic added a commit that referenced this issue Jul 23, 2018
domenic added a commit that referenced this issue Aug 1, 2018
alice pushed a commit to alice/html that referenced this issue Jan 8, 2019
mustaqahmed pushed a commit to mustaqahmed/html that referenced this issue Feb 15, 2019
mustaqahmed pushed a commit to mustaqahmed/html that referenced this issue Feb 15, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements
Development

No branches or pull requests