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

@MayaLekova MayaLekova commented Jun 28, 2018

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 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
Copy link
Member

@ljharb 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

LGTM, and seems like a great consistency improvement to use PromiseResolve() more. (Compare to Promise.prototype.finally.)

Copy link
Member

@zenparsing zenparsing left a comment

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

This comment has been minimized.

@zenparsing

zenparsing Jun 28, 2018
Member

PromiseResolve currently takes a constructor in the first argument position. I suppose that we'll need to provide it with %Promise%.

This comment has been minimized.

@benjamn

benjamn Jun 28, 2018
Member

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.

This comment has been minimized.

@zenparsing

zenparsing Jun 28, 2018
Member

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

This comment has been minimized.

@ljharb

ljharb Jun 28, 2018
Member

I believe it was an intentional design decision, discussed in committee, not to support subclassed Promises in await.

This comment has been minimized.

@ljharb

ljharb Jun 28, 2018
Member

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

This comment has been minimized.

@benjamn

benjamn Jun 28, 2018
Member

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.

This comment has been minimized.

@benjamn

benjamn Jun 28, 2018
Member

(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?

This comment has been minimized.

@zenparsing

zenparsing Jun 28, 2018
Member

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.

This comment has been minimized.

@ljharb

ljharb Jun 28, 2018
Member

I also think it's useful to keep the explicit %Promise%.

This comment has been minimized.

@MayaLekova

MayaLekova Jun 29, 2018
Author Contributor

Thanks for the discussion!
Added %Promise% as a parameter in a follow-up commit.

@zenparsing
Copy link
Member

@zenparsing zenparsing commented Jun 28, 2018

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

@benjamn
Copy link
Member

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

@MayaLekova MayaLekova force-pushed the MayaLekova:optimize-await branch from 33b6432 to 5ae98c0 Jul 12, 2018
@MayaLekova
Copy link
Contributor Author

@MayaLekova 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 hidden.

@zenparsing

This comment has been hidden.

@zenparsing
Copy link
Member

@zenparsing zenparsing commented Aug 29, 2018

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?

mykmelez pushed a commit to mykmelez/gecko that referenced this pull request Mar 12, 2019
… r=arai

This patch implements the proposal in this pull request:
<tc39/ecma262#1250>

Differential Revision: https://phabricator.services.mozilla.com/D21816
moz-v2v-gh pushed a commit to mozilla/gecko-dev that referenced this pull request Mar 13, 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

--HG--
extra : moz-landing-system : lando
mykmelez pushed a commit to mykmelez/gecko that referenced this pull request Mar 13, 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
moz-v2v-gh pushed a commit to mozilla/gecko-dev that referenced this pull request Mar 27, 2019
… r=arai

This patch implements the change in this pull request:
<tc39/ecma262#1250>

Differential Revision: https://phabricator.services.mozilla.com/D21816

--HG--
extra : moz-landing-system : lando
mykmelez pushed a commit to mykmelez/gecko that referenced this pull request Mar 27, 2019
… r=arai

This patch implements the change in this pull request:
<tc39/ecma262#1250>

Differential Revision: https://phabricator.services.mozilla.com/D21816
ljharb added a commit to jmdyck/ecma262 that referenced this pull request Sep 16, 2019
The ID of this section was changed, but the previous ID was not retained
in "oldids".
ljharb added a commit to ljharb/ecma262 that referenced this pull request Sep 16, 2019
 - 2016: the Unicode change affected what was considered whitespace (tc39#300 / 24dad16)
 - 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 Sep 16, 2019
 - 2016: the Unicode change affected what was considered whitespace (tc39#300 / 24dad16)
 - 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 Sep 17, 2019
 - 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 1, 2019
 - 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)
gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified that referenced this pull request Oct 4, 2019
… r=arai

This patch implements the proposal in this pull request:
<tc39/ecma262#1250>

Differential Revision: https://phabricator.services.mozilla.com/D21816

UltraBlame original commit: a14fcb229ddd59f9d7efb4f3b086503e804dfe8c
gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified 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-comments-removed that referenced this pull request Oct 4, 2019
… r=arai

This patch implements the proposal in this pull request:
<tc39/ecma262#1250>

Differential Revision: https://phabricator.services.mozilla.com/D21816

UltraBlame original commit: a14fcb229ddd59f9d7efb4f3b086503e804dfe8c
gecko-dev-updater pushed a commit to marco-c/gecko-dev-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-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
gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified 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
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 proposal in this pull request:
<tc39/ecma262#1250>

Differential Revision: https://phabricator.services.mozilla.com/D21816

UltraBlame original commit: a14fcb229ddd59f9d7efb4f3b086503e804dfe8c
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

@joshuaaguilar20 joshuaaguilar20 commented Sep 24, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet