-
Notifications
You must be signed in to change notification settings - Fork 18
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
Return a Promise<void> instead of a boolean #15
Comments
Using promises here has been brought up a few times in various forums. However, there are concerns since instantiating a promise object takes time as does switching from macro to micro tasks. This API is designed to be called many times in a loop within hot code, so it needs to be able to run really quickly and efficiently. Using a promise here would hurt that premise. In terms of the promise semantics being easier to understand, someone could absolutely build some kind of promised based API on top the low level API we're proposing here :) |
Aren't we missing the "wait until no input pending" bit, or does |
I believe so, yes -- spinning the event loop in |
Given that it waits for tasks other than input tasks, and waits this extra user-agent defined length of time, it doesn't seem like it compliments |
Do you have data on the performance characteristics on both approaches? Given that I was not the first one to propose Promises, it seems worthwhile to investigate what the actual consequences are. I would personally be surprised if (for a long-running task that already blocks user input and thus requires yielding to the browser) the overhead of a Promise would be significant. |
Sorry for the huge delay here, this totally fell off my radar.
For some context, the problem isn't that it's expensive on calls that cause yielding -- we actually need to be concerned that the API is expensive when it doesn't force a yield. This is by far the most common case we can expect. Consider the case where there's no input pending, and we call a promise-based isInputPending implementation. We force ourselves to bite the performance impact of allocating + chaining a new promise here, irrespective of the fact that there's nothing we had to yield for. In a release build of Chromium with a naive/idiomatic implementation of this (just allocating the promise, without even chaining), this reduces our ops/s by around 70% [1] using an idiomatic promise resolution strategy. Granted, there's probably room for optimization here, but for such a performance-critical API it seems like we'd want to constrain the UA as little as possible -- especially given that we can achieve the desired semantics using the method described below.
Revisiting this, I think this case is now covered by changes to the spec earlier this year to have isInputPending return true if the UA has enqueued a task to dispatch input. Consider the following:
Therefore, a setTimeout call after isInputPending returns true will have the resulting task dispatch after any pending input tasks are dispatched. [1] Link to CL here: https://chromium-review.googlesource.com/c/chromium/src/+/2422703 Benchmark results (from the
|
There isn't a single task queue. Input and |
Since Promise allocation here is only required in the minority case and is expensive, maybe it's worthwhile to spin it to its own method? e.g. have a That way, developers could do something like:
|
Should |
Ah, good point.
Now that I think about it, I believe the goals of such a Perhaps it makes more sense to pursue the yielding component of this API on that front, which also services the proposed high-level scheduling API? cc @shaseley |
That does indeed look like a good fit... |
With The use case here is "I want to perform main thread work while there isn't an input event of a particular category pending". This proposal provides the "you need to stop" part of that, but not "now you can continue" - that requires polling by queuing tasks. The solution seems incomplete if the "you need to stop" and "now you can continue" APIs are referring to different things, no? |
I'm not sure we want to permit restricting yielding at an event type granularity, anyway -- that seems like it might introduce semantic issues with task ordering. |
But |
I don't think defining yielding specificity is as important for the use cases of this API. The goal is to reduce input latency in scheduler work loops, which I'm not sure a finer level of yielding granularity would offer us other than a strange contract with developers (though the consistency is appealing). On the detection front, letting isInputPending ignore continuous events is important for only handling the case of a button press during a large amount of work, but the UA should have the discretion to queue additional work it deems necessary in response to the input as well (e.g. highlights), which is why I think a method to yield only to a single class of input events is tricky. IMO |
For specifying what you want to wait for when yielding, the conversation here feels increasingly out of scope for this API. isInputPending is about if you should yield or not, |
Right, that part sounds fine to me. However, it is unclear to me how as a webdev I should act on this result. That sounds in scope to me. Whether |
A couple quick notes on the status of
|
Not sure if this has been considered, but reading the proposed API makes me wonder if the API can return a Promise instead that is resolved once input is no longer pending (for the provided
InputType
):To my understanding, this would solve ~3 problems that I thought of when analyzing the boolean-returning proposal.
It is unclear when input is no longer pending
With
isInputPending
, if it returnstrue
, it is unclear at what point in time it no longer is pending. In the example defined in the README, asetTimeout(task, 0)
is used to wait and check again. However, if I am not mistaken,setTimeout
provides no guarantee that input is no longer pending. Similarly, I think it does not provide a guarantee that tasks are further resolved in order whenisInputPending
was invoked.Control-flow is broken
Continueing on the previous point, if a
setTimeout
is invoked, the JavaScript following the original invocation ofdoWork
will continue.For example:
Since
doWork
breaks out of the while-loop, theperformSomeWorkThatActuallyReliesOnTheOutputOfDoWork
does not actually have any guarantee that the actions ofdoWork
have fully resolved. This problem is more prevalent ifdoWork
is put in a different file/library where the call-side does not know thatdoWork
is trying to be friendly to the browser by yielding. E.g. reading the following code would be problematic, as there is no way of knowing whetherdoWork
was actually finished:Granted, it is quite easy to work around these kind of issues, by wrapping the
setTimeout
in a Promise that you await, for which you makedoWork
async. However, as every call-side would need to do that, wrapping it in a Promise from the get-go would prevent that boilerplate.Completion violation
This is where I am not sure if it actually would resolve the issue outlined by @domenic in #12. My reading of a Promise-based API would mean that it would not violate the completion principle, but I might be missing internal details here.
isFramePending || isInputPending
->noBrowserTaskPending
All in all, the
isFramePending
proposal Discourse would have the same issue. Using them together could become quite clunky:If instead they would be unified to 1
noBrowserTaskPending
(naming of course up for discussion), the user could specify it like:This feels a lot more natural for both proposals, would circumvent the issues listed above (primarily the we-dont-know-when and control-flow concerns) and would unify the proposals to 1 API. Given that we might want to expose additional pending browser tasks in the future, the user would need to invoke every scheduler function separately which results in a lot of boilerplate.
The text was updated successfully, but these errors were encountered: