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

Normative: Reduce the number of ticks in async/await #1250

Merged
merged 3 commits into from
Feb 26, 2019

Conversation

MayaLekova
Copy link
Contributor

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 fragments
will differ in the number of iterations through the job queue are taken: because
await always wraps a Promise with another Promise, there are three job
queue items enqueued and dequeued before calling f in the await 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 replacing
the unconditional wrapping with a call to PromiseResolve. Analogous changes
are made in async iterators.

The patch preserves key design goals of async/await:

  • Job queue processing remains deterministic, including both ordering and the number of jobs enqueued (which is observable by interspersing other jobs)
  • Userland Promise libraries with "then" methods ("foreign thenables") are usable within await, and trigger a turn of the job queue
  • Non-Promises can be awaited, and this takes a turn of the native job queue (as do all usages of await)

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]:

  • The doxbee async/await performance benchmark [2] improved with 48%
  • The fibonacci async/await performance benchmark [3] improved with 23%
  • The Hapi throughput benchmark [4] improved with 50% (when async hooks are enabled) and with 20% (when async hooks are disabled)

[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

@MayaLekova
Copy link
Contributor Author

MayaLekova commented Jun 28, 2018

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!

@ljharb ljharb added the normative change Affects behavior required to correctly evaluate some ECMAScript source text label Jun 28, 2018
@ljharb
Copy link
Member

ljharb commented Jun 28, 2018

I like that this change leaves await robust, and uses the existing PromiseResolve mechanism. I think this also relates to #1118.

Copy link
Member

@domenic domenic left a 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.)

@ljharb ljharb added the needs consensus This needs committee consensus before it can be eligible to be merged. label Jun 28, 2018
@ljharb ljharb requested a review from benjamn June 28, 2018 15:25
Copy link
Member

@zenparsing zenparsing left a 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*, &laquo; _promise_ &raquo;).
1. Let _promise_ be ? PromiseResolve(&laquo; _value_ &raquo;).
Copy link
Member

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%.

Copy link
Member

@benjamn benjamn Jun 28, 2018

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 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.

Copy link
Member

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).

Copy link
Member

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.

Copy link
Member

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)

Copy link
Member

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.

Copy link
Member

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(&laquo; _value_ &raquo;)) works as written?

Copy link
Member

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.

Copy link
Member

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%.

Copy link
Contributor Author

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.

@zenparsing
Copy link
Member

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 awaited (because we are doing PerformPromiseThen directly):

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?

@ljharb
Copy link
Member

ljharb commented Jun 28, 2018

Since it’s only observable if you monkeypatch the then method, and since Promise.resolve behaves this way already, and since await is often described as conceptually wrapping its operand in Promise.resolve, it seems like the proper choice to me - and anyone relying on this behavior already can’t necessarily rely on it happening.

@benjamn

This comment has been minimized.

@benjamn
Copy link
Member

benjamn commented Jun 28, 2018

@ljharb You're right, monkey-patching Promise.prototype.then is already broken in the sense that .then doesn't get called by await reliably enough to do anything useful, such as automatically propagating some sort of async context data.

An alternative approach is to wrap await expressions within the async function body (or use a transform that does the wrapping automatically). For example, await <expr> could be transformed to captureContext()(await <expr>), where captureContext is something like:

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 AsyncResource API, but I very much agree that wrapping Promise.prototype.then is not the way to get there, so it's fine that this PR slightly interferes with that strategy.

@ljharb ljharb added the needs test262 tests The proposal should specify how to test an implementation. Ideally via github.com/tc39/test262 label Jun 29, 2018
@MayaLekova
Copy link
Contributor Author

MayaLekova commented Jul 12, 2018

Rebased the commits on top of master.

Here are the links to the original changes, for the purpose of comment tracking:
Version 1
Version 2

Big thanks to @littledan for wording the commit message!

@tolmasky

This comment has been minimized.

@zenparsing

This comment has been minimized.

@zenparsing
Copy link
Member

Following up on this.

At the previous TC39 meeting, @erights brought up a use case for monkey-patching Promise.prototype.then. If I remember correctly, a user may want to monkey-patch then such that it always returns a frozen promise object.

Does this change negatively impact that use case? I don't think so. Even with the current semantics, the promise that gets returned from p.then as a result of await p isn't observable to user code.

In general, a "useful" then monkey-patch is going to do one or more of the following:

  • Create side-effects
  • Modify the return value
  • Wrap the input callbacks

I think we are correct to discourage side effects triggered by await, for performance reasons.

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 await p. Because calling those functions will never result in the execution of user code, it's difficult to see how wrapping them can be useful currently.

It seems to me that this change does not significantly impact "reasonable" monkey-patching use cases.

Thoughts?

gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified-and-comments-removed that referenced this pull request Oct 4, 2019
…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
gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified-and-comments-removed that referenced this pull request Oct 4, 2019
… 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
ljharb added a commit to ljharb/ecma262 that referenced this pull request Oct 17, 2019
…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)
ljharb added a commit to ljharb/ecma262 that referenced this pull request Oct 17, 2019
…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)
ljharb added a commit to ljharb/ecma262 that referenced this pull request Oct 18, 2019
…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)
@joshuaaguilar20
Copy link

@MayaLekova thanks for this beautiful write up. This was a great addition to node

jridgewell added a commit to jridgewell/ecma262 that referenced this pull request May 9, 2022
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
jridgewell added a commit to jridgewell/ecma262 that referenced this pull request May 9, 2022
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
Jarred-Sumner added a commit to oven-sh/WebKit that referenced this pull request May 28, 2022
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();
```
webkit-commit-queue pushed a commit to WebKit/WebKit that referenced this pull request May 29, 2022
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
yaacovCR added a commit to graphql/graphql-js that referenced this pull request Dec 14, 2022
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
webkit-commit-queue pushed a commit to shvaikalesh/WebKit that referenced this pull request Dec 19, 2023
…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
eddyaics

This comment was marked as spam.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
has consensus This has committee consensus. has test262 tests normative change Affects behavior required to correctly evaluate some ECMAScript source text
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet