Closed syg closed 4 years ago
I'm not even sure you need the async
property if value
will always either be a string or a Promise?
@ljharb, did you see:
Last time this came up in SYNC-RESOLVE, the decision was that returning sometimes a Promise and sometimes a string is bad API design and is inconsistent with both JS APIs like Promise.all and all WebIDL APIs, which either don't return a Promise or always return a Promise.
(which is also my position, though not held all that strongly)
Or are you suggesting having just { value: string }
and { value: Promise }
, and making people do the typeof test themselves? Personally I would strongly prefer we avoid returning untagged unions, as a rule.
Yes, that was my suggestion.
Personally I would strongly prefer we avoid returning untagged unions, as a rule.
That's my opinion as well.
FWIW, to a certain extent engines can optimise this to skip the extra microtask tick.
(I don't want to imply that other engines are required to implement a similar optimisation. And "to a certain extent" also means that this optimisation isn't always possible, so there can be performance cliffs.)
FWIW, to a certain extent engines can optimise this to skip the extra microtask tick.
Indeed. The operative condition there is that the microtask queue is empty at the point of the await
-- a good optimization when we can get it, but still prone to the performance footgun in the performance sensitive code that Atomics
is likely to be used for.
This received consensus in the April 2020 TC39, so merging.
This is an alternative to #29.
As I was playing around with the API during implementation and as prompted by Rick's async errors question from #28, I'm reopening the discussion from SYNC-RESOLVE, as the current design has a performance footgun.
The idea is instead of making everything async, return a wrapped result object that only has a Promise when the action could not be completed synchronously.
The Point of
Atomics.wait
and FutexAtomics.wait
is basically a futex emulation layer in JS. Futexes are a Linux kernel concept designed for writing userland synchronization data structures, mostly mutexes. The core idea is to provide a low-level building block so mutexes can tell the kernel to do the slow thing of blocking the thread and waiting for the mutex to be unlocked only when it's necessary. IOW, the point is to be fast when there's no contention on the lock.A usual example, taken from this futex article, looks something like this, converted to JS:
Crucial to the performance of this mutex is that the
Atomics.wait
only does something expensive like waiting when there is actually contention, i.e. whensab[ATOM_INDEX] == 2
. The API checks for the condition and decides to wait or not wait in one atomic action. Whensab[ATOM_INDEX] != 2
, it returns"not-equal"
immediately for another go around the do-while loop.That fail-fast behavior lets the mutex deal with fast unlocking: if another thread unlocked the mutex in the meantime, we didn't do an expensive wait for nothing and degraded the performance of our multithreaded code.Performance Footgun with Always Returning a Promise
Now imagine that
Atomics.wait
replaced withAtomics.waitAsync
that always returns a Promise:If
Atomics.waitAsync
always returns a Promise,await Atomics.waitAsync(...)
always takes at least 1 microtask tick. This could be slow enough that it destroys the performance model of the mutex.We can work around it by doing another
Atomics.load
to decrease the chances of the value from changing, but it's not a perfect workaround. There will always be a TOCTOU problem:This PR
The alternative is to sometimes return a Promise so the mutex code above can continue to "fail fast" when it doesn't need to actually wait. Since conditional waiting is the whole point of the futex wait API, this conditionally returning a Promise is consistent with that mental model: just in the case when it needs to wait, you get a Promise that's fulfilled when the wait is finished.
Last time this came up in SYNC-RESOLVE, the decision was that returning sometimes a Promise and sometimes a string is bad API design and is inconsistent with both JS APIs like
Promise.all
and all WebIDL APIs, which either don't return a Promise or always return a Promise.This PR instead, on @bakkot's suggestion, returns a wrapped result object like iterator results:
Note that in the async case, the Promise can only be resolved with "ok" or "timed out".
By wrapping, users that don't care about the optimization opportunity of failing fast on
"not-equal"
can await.value
:Since it'll no longer always return a Promise, argument validation errors will remain synchronous.
Why It Should Be a Decision We Make Now
The alternatives are:
(1) is undesirable because it goes against the point of the API for the primary use case: writing mutexes, where we know the performance does matter. I'd rather not ship an API that requires a workaround for the primary use case.
(2) is undesirable because
Atomics.waitAsync
always returning a Promise precludes amending the method with flags in the future that can fail fast. That points to that if the performance use case becomes apparently important afterAtomics.waitAsync
ships, the only way to accommodate is to add a 3rd method in addition to bothAtomics.wait
andAtomics.waitAsync
.(3) is this PR. It's undesirable because that makes this API more complicated and harder to use, though it is a more niche and power-user API.