-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Normative: Reduce the number of ticks in async/await #1250
Conversation
Please note a possible issue of this change noted by @zenparsing in the original commit. Any feedback on what's the importance of this issue and suggestions how it can be worked around are more than welcome! |
I like that this change leaves |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, and seems like a great consistency improvement to use PromiseResolve() more. (Compare to Promise.prototype.finally.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the detailed write-up and benchmarks!
spec.html
Outdated
</emu-alg> | ||
|
||
<p>mean the same thing as:</p> | ||
|
||
<emu-alg> | ||
1. Let _asyncContext_ be the running execution context. | ||
1. Let _promiseCapability_ be ! NewPromiseCapability(%Promise%). | ||
1. Perform ! Call(_promiseCapability_.[[Resolve]], *undefined*, « _promise_ »). | ||
1. Let _promise_ be ? PromiseResolve(« _value_ »). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PromiseResolve
currently takes a constructor in the first argument position. I suppose that we'll need to provide it with %Promise%
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the chances we could tolerate any Promise
subclass here? In other words, pass value.constructor
as the first argument to PromiseResolve
if value instanceof %Promise%
(pardon my hand waving; there may be a better spec notation for this), thereby giving value a chance to be returned as-is by PromiseResolve
.
I realize the async
function can only return instances of the original, native Promise
constructor, but it seems like a good thing for await
to treat Promise
subclasses the same way it treats ordinary Promise
s.
Unless part of the deal with subclassing Promise
is that you're guaranteed your .prototype.then
method will be called by await
?
I don't have strong feelings either way, but it seems like a discussion worth having.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A Promise subclass would (presumably) fail the SameValue
test on 2.b of PromiseResolve
, and we would then fallback to creating a new promise (exactly as we currently do).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe it was an intentional design decision, discussed in committee, not to support subclassed Promises in await
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Specifically, using .constructor here would mean that any object coercible value in the language would pass the test in PromiseResolve, which would break the semantics)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So it's exactly as if await
calls Promise.resolve
, which passes this
as the constructor argument to PromiseResolve
. Although this
could be a subclass of Promise
in arbitrary user code, it would always be %Promise%
in an async
function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Specifically, using .constructor here would mean that any object coercible value in the language would pass the test in PromiseResolve, which would break the semantics)
That's why we would have to perform an external check that .constructor
is a Promise
subclass before calling PromiseResolve(_value_.constructor, _value_)
, or perhaps modify PromiseResolve
to enforce the subclass relationship.
How do folks feel about introducing a single-argument PromiseResolve
variant that does the right thing (whatever we decide that is), so that this code (i.e. Let _promise_ be ? PromiseResolve(« _value_ »)
) works as written?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would probably prefer the explicit constructor argument to PromiseResolve
for now. And I believe it should be %Promise%
in any case: subclasses should go through the slower but semantically more flexible path of calling their overridden "then" method from a new tick.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also think it's useful to keep the explicit %Promise%
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the discussion!
Added %Promise%
as a parameter in a follow-up commit.
I'm curious about what the group thinks about the issue that @MayaLekova referenced upthread: with these new semantics "then" will not be called on a native, non-subclassed promise when async function f() {
let promise = Promise.resolve(1);
promise.then = function(...args) {
print('then called');
return Promise.prototype.then.apply(this, args);
};
let result = await promise;
return result;
}
f().then(v => print(v)); Currently, we log "then called", but I think after the change we will not. Is this edge case a problem that we should worry about? |
Since it’s only observable if you monkeypatch the then method, and since Promise.resolve behaves this way already, and since |
This comment has been minimized.
This comment has been minimized.
@ljharb You're right, monkey-patching An alternative approach is to wrap function captureContext() {
const context = getCurrentContext();
return function restoreContext(value) {
setCurrentContext(context);
return value;
};
} I hope we (TC39) can keep entertaining potential solutions for this kind of use case, such as Node's |
33b6432
to
5ae98c0
Compare
Rebased the commits on top of master. Here are the links to the original changes, for the purpose of comment tracking: Big thanks to @littledan for wording the commit message! |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
5ae98c0
to
05a63be
Compare
Following up on this. At the previous TC39 meeting, @erights brought up a use case for monkey-patching Does this change negatively impact that use case? I don't think so. Even with the current semantics, the promise that gets returned from In general, a "useful"
I think we are correct to discourage side effects triggered by A monkey-patch that modifies the return value (e.g. the frozen promise use case) will not be impacted by this change, since the return value is always discarded anyway. What about a monkey-patch that wraps the input callbacks (e.g. for "async zone" tracking)? Under the current semantics, such a monkey-patch will only see a native "resolve" and "reject" function as a result of It seems to me that this change does not significantly impact "reasonable" monkey-patching use cases. Thoughts? |
…t draft spec. r=anba The new steps are official since <tc39/ecma262#1250> landed. (Some of these step numbers change again in the next commit.) Differential Revision: https://phabricator.services.mozilla.com/D23029 UltraBlame original commit: 47e570e513851c5e4a7b930af82ad9de21e0bb22
… r=arai This patch implements the change in this pull request: <tc39/ecma262#1250> Differential Revision: https://phabricator.services.mozilla.com/D21816 UltraBlame original commit: 269654f1eeb2b6e099af2e5e3e48d07bc1488268
…ns (tc39#1698) - 2016: the Unicode change affected what was considered whitespace (tc39#300 / 24dad16) - 2017: the latest version of Unicode is mandated (tc39#620) - 2018: changed tagged template literal objects to be cached per source location rather than per realm (tc39#890) - 2019: Atomics.wake was renamed to Atomics.notify (tc39#1220) - 2019: `await` was changed to require fewer ticks (tc39#1250)
…ns (tc39#1698) - 2016: the Unicode change affected what was considered whitespace (tc39#300 / 24dad16) - 2017: the latest version of Unicode is mandated (tc39#620) - 2018: changed tagged template literal objects to be cached per source location rather than per realm (tc39#890) - 2019: Atomics.wake was renamed to Atomics.notify (tc39#1220) - 2019: `await` was changed to require fewer ticks (tc39#1250)
…ns (tc39#1698) - 2016: the Unicode change affected what was considered whitespace (tc39#300 / 24dad16) - 2017: the latest version of Unicode is mandated (tc39#620) - 2018: changed tagged template literal objects to be cached per source location rather than per realm (tc39#890) - 2019: Atomics.wake was renamed to Atomics.notify (tc39#1220) - 2019: `await` was changed to require fewer ticks (tc39#1250)
@MayaLekova thanks for this beautiful write up. This was a great addition to node |
This aligns the behavior of handling thenables with the Promises A+ spec, which says when the resolution value is a thenable, you're supposed to synchronously call `thenable.then(onRes, onRej)` ([Step 2.3.3.3][call-then]). Given the example code: ```javascript new Promise(resolve => { resolve(thenable) }); ``` The current behavior requires 2 ticks for the outer promise to fully resolve. One tick is created tick is created by the Promise Resolving Functions (and is removed in this PR), and one tick is created by the wrapping of the `onRes`/`onRej` callbacks passed to [`Promise.p.then`][then]. This made it noticeably slower to resolve with a thenable then to invoke the thenable's `.then` directly: `thenable.then(onRes, onRej)` only requires a single tick (for the wrapped `onRes`/`onRej`). With this change, we could revert tc39#1250 without slowing down `Await`. Fixes tc39#2770. [call-then]: https://promisesaplus.com/#point-56 [then]: https://tc39.es/ecma262/multipage/control-abstraction-objects.html#sec-performpromisethen
This aligns the behavior of handling thenables with the Promises A+ spec, which says when the resolution value is a thenable, you're supposed to synchronously call `thenable.then(onRes, onRej)` ([Step 2.3.3.3][call-then]). Given the example code: ```javascript new Promise(resolve => { resolve(thenable) }); ``` The current behavior requires 2 ticks for the outer promise to fully resolve. One tick is created by the Promise Resolving Functions (and is removed in this PR), and one tick is created by the wrapping of the `onRes`/`onRej` callbacks passed to [`Promise.p.then`][then]. This made it noticeably slower to resolve with a thenable than to invoke the thenable's `.then` directly: `thenable.then(onRes, onRej)` only requires a single tick (for the wrapped `onRes`/`onRej`). With this change, we could revert tc39#1250 without slowing down `Await`. Fixes tc39#2770. [call-then]: https://promisesaplus.com/#point-56 [then]: https://tc39.es/ecma262/multipage/control-abstraction-objects.html#sec-performpromisethen
This makes promises & async take two ticks instead of three ticks. See tc39/ecma262#1250 See tc39/ecma262#2770 See tc39/ecma262#2772 50% faster when measuring call overhead. Similar improvements for https://github.com/v8/promise-performance-tests and when measuring with the following snippet: ```js import { run, bench } from "mitata"; bench("sync", () => {}); bench("async", async () => {}); run(); ```
https://bugs.webkit.org/show_bug.cgi?id=241072 Reviewed by Saam Barati. This patch integrates spec change[1], which removes one level tick count when resolving promise with await. Previously, regardless of whether the value is promise or not, we are always using resolveWithoutPromise, but it introduces one tick before the handlers are resolved. The spec change makes it that we can call performPromiseThen directly if the input value is promise, so we can skip one tick which looks up "then" and register handlers. This is beneficial for await performance and it also fixes a bug tested via test262 and attached test due to the spec change. We observed performance improvement in async + native promise tests. ToT Time(doxbee-async-es2017-native): 35.6 ms. Time(fibonacci-async-es2017-native): 292.3 ms. Time(parallel-async-es2017-native): 117.3 ms. Patched Time(doxbee-async-es2017-native): 24.2 ms. Time(fibonacci-async-es2017-native): 198.1 ms. Time(parallel-async-es2017-native): 109.5 ms. [1]: tc39/ecma262#1250 * JSTests/stress/async-await-basic.js: * JSTests/stress/async-await-tick-count.js: Added. (shouldBe): (async returnDirectPrimitive): (async returnAwaitPrimitive): (async returnDirectPromisePrimitive): (async returnAwaitPromisePrimitive): (async test): (async tests): (globalThis.setUnhandledRejectionCallback.setUnhandledRejectionCallback): * JSTests/test262/expectations.yaml: * LayoutTests/inspector/canvas/recording-bitmaprenderer-frameCount-expected.txt: * LayoutTests/inspector/canvas/recording-bitmaprenderer-full-expected.txt: * LayoutTests/inspector/canvas/recording-bitmaprenderer-memoryLimit-expected.txt: * LayoutTests/inspector/console/message-stack-trace-expected.txt: * Source/JavaScriptCore/builtins/AsyncFromSyncIteratorPrototype.js: * Source/JavaScriptCore/builtins/AsyncFunctionPrototype.js: (globalPrivate.asyncFunctionResume): * Source/JavaScriptCore/builtins/AsyncGeneratorPrototype.js: (globalPrivate.awaitValue): (globalPrivate.asyncGeneratorResumeNext): * Source/JavaScriptCore/builtins/PromiseOperations.js: (globalPrivate.newPromiseCapabilitySlow): (globalPrivate.promiseResolve): (globalPrivate.promiseResolveSlow): (globalPrivate.promiseRejectSlow): (globalPrivate.resolvePromiseWithFirstResolvingFunctionCallCheck): (globalPrivate.fulfillPromiseWithFirstResolvingFunctionCallCheck): (globalPrivate.rejectPromiseWithFirstResolvingFunctionCallCheck): (globalPrivate.resolveWithoutPromiseForAsyncAwait): Canonical link: https://commits.webkit.org/251106@main git-svn-id: https://svn.webkit.org/repository/webkit/trunk@295011 268f45cc-cd09-0410-ab3c-d52691b4dbfc
Promise resolution order changes in some instances, resulting in different orders for some errors within the errors array, as well as in different values of hasNext within incremental delivery payloads. This PR introduces an async `completePromisedValue` helper function rather than using a promise chain (see below links). https://github.com/tc39/proposal-faster-promise-adoption tc39/ecma262#2770 tc39/ecma262#2772 tc39/ecma262#1250 https://v8.dev/blog/fast-async Depends on #3793
…omises https://bugs.webkit.org/show_bug.cgi?id=266502 <rdar://problem/119734587> Reviewed by Justin Michaud. Before this change, abrupt completions of PromiseResolve [1] that arised during "constructor" lookup were not handled properly in async functions and generators, resulting in exception propagation up the call stack rather than rejecting a promise. That affected `await`, `yield`, and `return` called with a broken promise (i.e. with throwing "constructor" getter). Most likely, this is a regression from implementing async / await tick reduction proposal [2]. This patch guards "constructor" lookup with exception handling, ensuring that all call sites supply onRejected() callback that is semantically equivalent to throwing an exception at that point, as per spec. Invoking onRejected() synchronously, without extra microtask, is also required to match the standard, V8, and SpiderMonkey. Also, this change implements a proposal [3] to fix AsyncGenerator.prototype.return() called on a broken promise, aligning JSC with V8. [1]: https://tc39.es/ecma262/#sec-promise-resolve (step 1.a) [2]: tc39/ecma262#1250 [3]: tc39/ecma262#2683 * JSTests/stress/async-function-broken-promise.js: Added. * JSTests/test262/expectations.yaml: Mark 4 tests as passing. * Source/JavaScriptCore/builtins/PromiseOperations.js: (linkTimeConstant.resolveWithoutPromiseForAsyncAwait): Canonical link: https://commits.webkit.org/272291@main
JavaScript programmers may expect that the following
two programs are largely similar in terms of how they perform with
respect to the ECMAScript job queue (if inside of an async function):
promise.then(f); f(await promise);
However, if
promise
is a built-in promise, then these two code fragmentswill differ in the number of iterations through the job queue are taken: because
await
always wraps a Promise with another Promise, there are three jobqueue items enqueued and dequeued before calling
f
in theawait
example,whereas there is just a single item for the
then
usage.In discussions with JavaScript programmers, the number of job queue items
in the current semantics turns out to be surprising. For example, the difference
has become more visible in conjunction with new V8 features for Promise
visibility and debugging, which are sometimes used in Node.js.
This patch changes the semantics of await to reduce the number of
job queue turns that are taken in the common
await
Promise case by replacingthe unconditional wrapping with a call to PromiseResolve. Analogous changes
are made in async iterators.
The patch preserves key design goals of async/await:
await
, and trigger a turn of the job queueawait
)Reducing the number of job queue turns also improves performance
on multiple highly optimized async/await implementations. In a draft
implementation of this proposal in V8 behind a flag [1]:
[1] https://chromium-review.googlesource.com/c/v8/v8/+/1106977
[2] https://github.com/bmeurer/promise-performance-tests/blob/master/lib/doxbee-async.js
[3] https://github.com/bmeurer/promise-performance-tests/blob/master/lib/fibonacci-async.js
[4] https://github.com/fastify/benchmarks