Normative: Reduce the number of ticks in async/await#1250
Normative: Reduce the number of ticks in async/await#1250ljharb merged 3 commits intotc39:masterfrom
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 |
domenic
left a comment
There was a problem hiding this comment.
LGTM, and seems like a great consistency improvement to use PromiseResolve() more. (Compare to Promise.prototype.finally.)
zenparsing
left a comment
There was a problem hiding this comment.
Thanks for the detailed write-up and benchmarks!
spec.html
Outdated
| 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.
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.
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 Promises.
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.
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.
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.
(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.
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.
(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.
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.
I also think it's useful to keep the explicit %Promise%.
There was a problem hiding this comment.
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? |
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
… r=arai This patch implements the proposal in this pull request: <tc39/ecma262#1250> Differential Revision: https://phabricator.services.mozilla.com/D21816
… r=arai This patch implements the change in this pull request: <tc39/ecma262#1250> Differential Revision: https://phabricator.services.mozilla.com/D21816
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 graphql#3793
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 graphql#3793
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 graphql#3793
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 graphql#3793
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 graphql#3793
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 graphql#3793
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
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
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 graphql#3793
…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
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
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
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
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
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
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
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
promiseis a built-in promise, then these two code fragmentswill differ in the number of iterations through the job queue are taken: because
awaitalways wraps a Promise with another Promise, there are three jobqueue items enqueued and dequeued before calling
fin theawaitexample,whereas there is just a single item for the
thenusage.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
awaitPromise 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