Closed Bug 1530324 Opened 6 years ago Closed 6 years ago

Remove wrapper functions for async functions/generators

Categories

(Core :: JavaScript Engine, enhancement)

enhancement
Not set
normal

Tracking

()

RESOLVED FIXED
mozilla67
Tracking Status
firefox67 --- fixed

People

(Reporter: anba, Assigned: anba)

References

Details

Attachments

(8 files, 4 obsolete files)

55.13 KB, patch
arai
: review+
Details | Diff | Splinter Review
34.36 KB, patch
arai
: review+
Details | Diff | Splinter Review
75.33 KB, patch
arai
: review+
Details | Diff | Splinter Review
21.93 KB, patch
arai
: review+
Details | Diff | Splinter Review
16.63 KB, patch
anba
: review+
Details | Diff | Splinter Review
36.10 KB, patch
anba
: review+
Details | Diff | Splinter Review
38.74 KB, patch
anba
: review+
Details | Diff | Splinter Review
63.26 KB, patch
anba
: review+
Details | Diff | Splinter Review

It doesn't look like it's too hard to remove the wrapper functions for async functions/generators. This should help to prevent me adding another sec-bug like bug 1528057. :-P

In preparation for later patches, the internal GeneratorObject for async functions is now stored in the PromiseReaction instead of the PromiseObject. This matches the existing behaviour for async generators and removes the (shared) PromiseSlot_AwaitGenerator slot from PromiseObject.

Later patches will also clean-up some of the extra lines introduced here, so this more or less all just a transient state.

Attachment #9046328 - Flags: review?(arai.unmht)
Comment on attachment 9046328 [details] [diff] [review] bug1530324-part1-asynfn-generator-in-promise-reaction.patch Review of attachment 9046328 [details] [diff] [review]: ----------------------------------------------------------------- Nice cleanup :D ::: js/src/builtin/Promise.cpp @@ +535,5 @@ > ReactionRecordSlot_Flags, > > // Additional slot to store extra data for specific reaction record types. > // > + // - When the REACTION_FLAG_ASYNC_FUNCTION flag is set, this slot store pre-existing tho, s/store/stores/, this and async generator comment.
Attachment #9046328 - Flags: review?(arai.unmht) → review+

Adds a new base class for generator objects called AbstractGeneratorObject. This isn't the most original name, but there's lot of existing code spread across the whole code base which refers to "generators", so it seemed useful to me to keep GeneratorObject in the name for the new base class.

This class will be used as the super-class for distinct generator objects created for generator functions, async functions, and async generator functions. For now there's only one concrete sub-class GeneratorObject, but the next patch will add more sub-classes.

I've used auto in a couple of places instead of AbstractGeneratorObject to avoid awkward line breaks, because of the longer class name AbstractGeneratorObject when compared to just GeneratorObject and the new 80 columns line length limit.

Drive-by change:

  • Cleaned up some #includes.
  • Cleaned access to RESUME_INDEX_SLOT in debugger code.
Attachment #9046335 - Flags: review?(arai.unmht)

Adds new generator object sub-classes for async functions (AsyncFunctionGeneratorObject) and async generators (AsyncGeneratorGeneratorObject). AsyncGeneratorGeneratorObject will later be combined with the existing AsyncGeneratorObject, so the silly "GeneratorGenerator" name won't exist for long :-)

Also adds specialised self-hosted functions to resume these generator objects, because the extra suspension checks and generator closing steps in Generator[Next,Return,Throw] aren't needed here. (Or actually the closing steps happen now in native code.)

This patch adds extra checks for the generator result value in WrappedAsyncFunction and WrappedAsyncGenerator, these will be removed later on. (The new test "js/src/jit-test/tests/debug/onEnterFrame-async-resumption-06.js" actually asserts on current tip, too!)

The various "js/src/jit-test/tests/debug/onEnterFrame-async-resumption-*" tests will change a few times with next patches, so (light spoiler alert!) you don't need to worry about Skynet too much. :-D

Attachment #9046340 - Flags: review?(arai.unmht)

Combines AsyncGeneratorObject and AsyncGeneratorGeneratorObject, and then removes the extra native wrapper for async generators.

For the most part only code removal:

  • Some places now need extra checks for async functions (of the form isAsync && !isGenerator), these will be removed in a later patch.

Added a new jit-test for off-thread compilation of different function types which can be run from the shell. This is currently only covered implicitly in browser tests, but not when running jstest/jit-tests from the shell.

Attachment #9046345 - Flags: review?(arai.unmht)

More or less a continuation of part 1. The result promise for async functions is now allocated when creating the generator object of async functions and stored within that generator object.

Two things noteworthy:

  • This changes the allocation site of the promise, so we now need to use the parent frame for AutoSetAsyncStackForNewCalls in AsyncFunctionResume.
  • I'm using NewBuiltinClassInstance in AsyncFunctionGeneratorObject::create, because that ensures the allocation can use the new-object cache. The new-object cache can only be used when the proto is non-null. So even though we never expose the internal generator object for async functions, so maybe a more natural solution would be to use null as the proto, it's necessary to use the default proto (here: %ObjectPrototype%) for performance reasons.
Attachment #9046352 - Flags: review?(arai.unmht)

We need two additional changes before we can finally remove the wrapper for async functions:

  1. We need to fulfill resp. reject the async function result promise on return resp. when an exception was thrown.
  2. And we also need to perform Await directly in the async function bytecode.

This and the next patch will make these changes.


Adds new JSOP_ASYNCRESOLVE bytecode to resolve the result promise of async functions.

return statements now perform an additional JSOP_ASYNCRESOLVE to fulfill the result promise, that means for example we're going to emit the following bytecode sequence for return x.

  JSOP_GETGNAME "x"
  JSOP_SETRVAL
  JSOP_GETRVAL
  JSOP_GETALIASEDVAR ".generator"
  JSOP_ASYNCRESOLVE "fulfill"
  JSOP_SETRVAL
  JSOP_GETALIASEDVAR ".generator"
  JSOP_FINALYIELDRVAL

instead of

  JSOP_GETGNAME "x"
  JSOP_SETRVAL
  JSOP_GETALIASEDVAR ".generator"
  JSOP_FINALYIELDRVAL

(Obviously the JSOP_SETRVAL + JSOP_GETRVAL sequence is not optimal and maybe it's also more performant to perform some stack juggling to avoid the two JSOP_GETALIASEDVAR. But I thought for reviewing it made more sense to use the more minimal approach which requires less additional code changes and keep the peephole optimisations for follow-up bugs.)

Promise rejections due to exceptions are a bit more difficult to handle, because there's no explicit code location like a return statement which need to be adjusted. Instead this patch simply adds a big try-catch block around the whole function body (yuk!). This seems to be an okay-ish solution as far as interpreter and baseline performance is concerned, but maybe we'll need something better for Ion (because some Ion-optimisations are disabled in try-catch blocks). But since we don't yet Ion-compile async functions the try-catch block solution should work for now. (The back-up plan is to hook into the places where we're currently calling HandleClosingGeneratorReturn, reject the result promise and then perform a forced-return via js::jit::ForcedReturn.)

There's a TODO note in the patch for BytecodeEmitter, which will be removed in the next patch.

There are existing debugger tests which require us to handle debugger-inserted return/throw completions for async functions. And because we no longer handle exceptions/return values in the native async function wrapper, but now within the async function bytecode, the AdjustGeneratorResumptionValue debugger function had to be err... "adjusted" as well. In AdjustGeneratorResumptionValue we're now resolving the result promise, which can lead to enqueueing new promise jobs, so I had to move some calls to JS::AutoDebuggerJobQueueInterruption::runJobs to ensure any jobs enqueued while the debugger is running are run before any previous, user-generated promise jobs.

This uncovered an existing bug in js::InternalJobQueue::SavedQueue, where the js::InternalJobQueue::draining_ state wasn't properly reset. A regression test for this bug was added at "js/src/jit-test/tests/debug/save-queue-resets-draining.js" (fail in current tip).

And then there's also "js/src/jit-test/tests/debug/Frame-onStep-async-02.js" which started to fail with this patch, because of bug 1470558. I've added a band-aid to ScriptedOnPopHandler::onPop to kind of make it possible to detect await for debugger consumers, but the proper fix should still happen in bug 1470558.

I think that's all for this patch. Turned out to be a bit more tricky than the previous patches, but the final patch should make it worthwhile nonetheless! :-)

Attachment #9046367 - Flags: review?(arai.unmht)

This patch adds a new JSOP_ASYNCAWAIT bytecode for awaiting values in await. The important bit here isn't actually to move the awaiting part from native code to bytecode, but that the new opcode allows to remove JSOP_INITIALYIELD for async functions, so that when an async function is called, we no longer have need to suspend and then immediately resume the function execution.

JSOP_ASYNCAWAIT pushes the async function result promise on the stack, so that when await is called for the very first time and the execution is suspended, the top stack value (= the result promise) is passed via JSOP_AWAIT back to the caller, similar to how JSOP_INITIALYIELD passes the generator object to the caller code.

We're now emitting the following bytecode sequence for await x:

  JSOP_GETGNAME "x"
  JSOP_TRYSKIPAWAIT
  JSOP_NOT
  JSOP_IFEQ
  JSOP_JUMPTARGET
  JSOP_GETALIASEDVAR ".generator"
  JSOP_ASYNCAWAIT
  JSOP_GETALIASEDVAR ".generator"
  JSOP_AWAIT
  JSOP_DEBUGAFTERYIELD
  JSOP_JUMPTARGET

instead of

  JSOP_GETGNAME "x"
  JSOP_TRYSKIPAWAIT
  JSOP_NOT
  JSOP_IFEQ
  JSOP_JUMPTARGET
  JSOP_GETALIASEDVAR ".generator"
  JSOP_AWAIT
  JSOP_DEBUGAFTERYIELD
  JSOP_JUMPTARGET

Additionally we now can move JSOP_GENERATOR to the very top of the generated bytecode, even before initialising default parameters. This ensures the try-catch block for promise rejections added in the previous patch no longer needs to handle the case when ".generator" is not yet present.

And more debugger tests which had to be updated: Because JSOP_INITIALYIELD is no longer emitted, there's one less on-enter-frame event, so the test expectations had to be adjusted.

Attachment #9046372 - Flags: review?(arai.unmht)

And finally the last patch. Similar to patch 4 this is mostly code removal.


GlobalObject::initAsyncFunction was missing a call to JSObject::setDelegate, I've moved it to NewSingletonObjectWithFunctionPrototype, so we don't need to call setDelegate manually in all callers to NewSingletonObjectWithFunctionPrototype.

Attachment #9046374 - Flags: review?(arai.unmht)
Comment on attachment 9046335 [details] [diff] [review] bug1530324-part2-add-abstract-generatorobject-superclass.patch Review of attachment 9046335 [details] [diff] [review]: ----------------------------------------------------------------- ::: js/src/vm/GeneratorObject.cpp @@ +29,5 @@ > RootedValue pval(cx); > + RootedFunction fun(cx, frame.callee()); > + > + Rooted<AbstractGeneratorObject*> genObj(cx); > + if (!fun->isAsync()) { Can you add some more comment what kind of situation "then" and "else" branches correspond to? also in the later patch that adds more branches.
Attachment #9046335 - Flags: review?(arai.unmht) → review+
Attachment #9046340 - Flags: review?(arai.unmht) → review+
Comment on attachment 9046345 [details] [diff] [review] bug1530324-part4-remove-async-generator-wrapper.patch Review of attachment 9046345 [details] [diff] [review]: ----------------------------------------------------------------- ::: js/src/frontend/ObjectEmitter.h @@ +241,5 @@ > MOZ_MUST_USE bool prepareForComputedPropKey( > const mozilla::Maybe<uint32_t>& keyPos, Kind kind = Kind::Prototype); > MOZ_MUST_USE bool prepareForComputedPropValue(); > > + MOZ_MUST_USE bool emitInitHomeObject(bool isAsyncNonGenerator = false); I prefer not-having bool parameter as much as possible. but I suppose this method be changed again later ::: js/src/vm/JSFunction.cpp @@ +613,5 @@ > > if (mode == XDR_DECODE) { > RootedObject proto(cx); > + if ((firstword & IsGenerator) || (firstword & IsAsync)) { > + if ((firstword & IsGenerator) && (firstword & IsAsync)) { can you assign both `firstword & IsGenerator` and `firstword & IsAsync` to local variable?
Attachment #9046345 - Flags: review?(arai.unmht) → review+
Attachment #9046352 - Flags: review?(arai.unmht) → review+

will review remaining parts tonight.

Comment on attachment 9046367 [details] [diff] [review] bug1530324-part6-add-asyncresolve-bytecode-op.patch Review of attachment 9046367 [details] [diff] [review]: ----------------------------------------------------------------- I guess I won't be able to finish reviewing remaining 2 patches today. (Part 7 requires some more time for me to grok it) will try finish by the end of tomorrow. ::: js/src/vm/Opcodes.h @@ +2067,5 @@ > */ \ > MACRO(JSOP_CHECKTHISREINIT, 191, "checkthisreinit", NULL, 1, 1, 1, JOF_BYTE) \ > /* > + * Pops the top two values 'valueOrReason' and 'gen' from the stack, then > + * pushes the promise resolved with 'valueOrReason'. can you clarify the following? - `gen` is async function's internal generator - this opcode resolves `gen`'s promise
Attachment #9046367 - Flags: review?(arai.unmht) → review+
Comment on attachment 9046372 [details] [diff] [review] bug1530324-part7-remove-asyncfn-initial-yield.patch Review of attachment 9046372 [details] [diff] [review]: ----------------------------------------------------------------- ::: js/src/builtin/Promise.cpp @@ +4840,5 @@ > return false; > } > + // In the initial call into an async function, the parent frame isn't > + // AsyncFunctionResume. > + if (!iter.isFunctionFrame()) { Can you clarify that this branch doesn't completely filter out the initial call case? this filters out only the case that the async function is called from top-level code, and another cases are filtered out below. ::: js/src/jit-test/tests/debug/onEnterFrame-async-resumption-01.js @@ +18,5 @@ > hits++; > }; > > + // If we force-return a generator object here, the caller will receive > + // receive a promise object resolved with that generator. s/receive// ::: js/src/tests/non262/async-functions/ErrorStack.js @@ +9,5 @@ > > async function thrower() { > let stack = new Error().stack; // line 11 > assertEq(/^thrower@.+ErrorStack.js:11/m.test(stack), true, toMessage(stack)); > + assertEq(/^inner@.+ErrorStack.js:38/m.test(stack), true, toMessage(stack)); similar change might be necessary for mochitest tests if there's any ::: js/src/vm/AsyncFunction.cpp @@ +286,5 @@ > if (!obj) { > return nullptr; > } > obj->initFixedSlot(PROMISE_SLOT, ObjectValue(*resultPromise)); > + looks like here's white space? can you remove? ::: js/src/vm/Opcodes.h @@ +1625,5 @@ > MACRO(JSOP_POW, 150, "pow", "**", 1, 2, 1, JOF_BYTE|JOF_IC) \ > /* > + * Pops the top two values 'value' and 'gen' from the stack, then starts > + * "awaiting" for 'value' to be resolved, which will then resume the > + * execution of 'gen'. Pushes the async function promise on the stack. can you add comment or reference to another comment that describes how the pushed promise is used? it took me some time to figure out why this pushes it. (the promise is used only for the first await, and the promise is returned back to the caller, right?) here, and also to AsyncFunctionAwait return value. @@ +1635,2 @@ > */ \ > + MACRO(JSOP_ASYNCAWAIT, 151, "async-await", NULL, 1, 2, 1, JOF_BYTE) \ having JSOP_AWAIT and JSOP_ASYNCAWAIT is very confusing... how do you think renaming JSOP_AWAIT to JSOP_ASYNCSUSPEND? given now JSOP_AWAIT just suspends the execution.
Attachment #9046372 - Flags: review?(arai.unmht) → review+
Comment on attachment 9046374 [details] [diff] [review] bug1530324-part8-remove-async-function-wrapper.patch Review of attachment 9046374 [details] [diff] [review]: ----------------------------------------------------------------- ::: js/src/frontend/BytecodeEmitter.cpp @@ -3246,3 @@ > // [stack] NAME FUN > - // [stack] # isAsync && !isGenerator && needsHomeObject > - // [stack] NAME UNWRAPPED WRAPPED nice that this complexity disappears :D ::: js/src/vm/GeneratorObject.cpp @@ +229,5 @@ > return nullptr; > } > + RootedObject obj( > + cx, NewObjectWithGivenProto<PlainObject>(cx, proto, SingletonObject)); > + if (!obj || !JSObject::setDelegate(cx, obj)) { it would be better separating them into 2 if statements, given the conditions are not same
Attachment #9046374 - Flags: review?(arai.unmht) → review+
Blocks: 1473796

Updated part 1 per review comments, carrying r+.

Attachment #9046328 - Attachment is obsolete: true
Attachment #9046756 - Flags: review+

(In reply to Tooru Fujisawa [:arai] from comment #10)

Can you add some more comment what kind of situation "then" and "else"
branches correspond to?
also in the later patch that adds more branches.

With all patches applied the if-else statements look like

  Rooted<AbstractGeneratorObject*> genObj(cx);
  if (!fun->isAsync()) {
    genObj = GeneratorObject::create(cx, fun);
  } else if (fun->isGenerator()) {
    genObj = AsyncGeneratorObject::create(cx, fun);
  } else {
    genObj = AsyncFunctionGeneratorObject::create(cx, fun);
  }

which should be self-documenting. Do you have any specific comments in my mind which should be added here to improve clarity?

(In reply to Tooru Fujisawa [:arai] from comment #11)

  • MOZ_MUST_USE bool emitInitHomeObject(bool isAsyncNonGenerator = false);

I prefer not-having bool parameter as much as possible.
but I suppose this method be changed again later

Yes, later patches will remove the bool parameter. :-)

::: js/src/vm/JSFunction.cpp
@@ +613,5 @@

if (mode == XDR_DECODE) {
RootedObject proto(cx);

  • if ((firstword & IsGenerator) || (firstword & IsAsync)) {
  •  if ((firstword & IsGenerator) && (firstword & IsAsync)) {
    

can you assign both firstword & IsGenerator and firstword & IsAsync to
local variable?

I've already prepared, but not yet posted, a follow-up patch which will clean this up, therefore I'd like to keep this part as is for now.

Updated comments for JSOP_ASYNCRESOLVE per review comments, carrying r+.

Attachment #9046367 - Attachment is obsolete: true
Attachment #9046758 - Flags: review+

Updated part 7 per review comments, carrying r+.

(In reply to Tooru Fujisawa [:arai] from comment #14)

@@ +1635,2 @@

  */ \
  • MACRO(JSOP_ASYNCAWAIT, 151, "async-await", NULL, 1, 2, 1, JOF_BYTE) \

having JSOP_AWAIT and JSOP_ASYNCAWAIT is very confusing...

how do you think renaming JSOP_AWAIT to JSOP_ASYNCSUSPEND?
given now JSOP_AWAIT just suspends the execution.

Agreed, I'm not really happen to have both JSOP_AWAIT and JSOP_ASYNCAWAIT. I think I'd like to try to use the new JSOP_ASYNCAWAIT in async generators somehow, too. And if that works out, I'll try to merge JSOP_AWAIT and JSOP_ASYNCAWAIT, so that there's ultimately only a single JSOP_AWAIT opcode. But before we can do that, async generators need a few changes. For example this code should be changed for performance reasons (creating an iterator result object only to wrap the "value" property is a bit wasteful) and also for spec compliance reasons (the access to "value" needs to happen in the async generator execution): The following test case should print "SUCCESS", but currently throws an uncaught exception.

async function* f() {
  var token = {};
  try {
    yield* {
      [Symbol.asyncIterator]() {
        return {
          next() {
            return {
              done: false,
              get value() {
                throw token;
              }
            };
          }
        };
      }
    };
  } catch (e) {
    print((e === token) ? "SUCCESS" : "FAIL");
  }
}

f().next();
Attachment #9046372 - Attachment is obsolete: true
Attachment #9046765 - Flags: review+

Updated part 8 per review comments, carrying r+.

Attachment #9046374 - Attachment is obsolete: true
Attachment #9046766 - Flags: review+

Pushed by archaeopteryx@coole-files.de:
https://hg.mozilla.org/integration/mozilla-inbound/rev/1dd26ddfc373
Part 1: Store the internal generator of async functions in the Promise reaction. r=arai
https://hg.mozilla.org/integration/mozilla-inbound/rev/90d0e91224a9
Part 2: Add abstract super class for GeneratorObject. r=arai
https://hg.mozilla.org/integration/mozilla-inbound/rev/e125746cec95
Part 3: Add separate AbstractGeneratorObject subclasses for async functions and async generators. r=arai
https://hg.mozilla.org/integration/mozilla-inbound/rev/135c13d4ceba
Part 4: Remove wrapper function for async generators. r=arai
https://hg.mozilla.org/integration/mozilla-inbound/rev/48fb1e2b6e97
Part 5: Store result promise in the internal generator object of async functions. r=arai
https://hg.mozilla.org/integration/mozilla-inbound/rev/b84fd1d91da2
Part 6: Add JSOP_ASYNCRESOLVE to fulfill/reject an async function promise. r=arai
https://hg.mozilla.org/integration/mozilla-inbound/rev/dfcdd2084fea
Part 7: Remove initial-yield for async functions. r=arai
https://hg.mozilla.org/integration/mozilla-inbound/rev/55b6a8c4e015
Part 8: Remove wrapper function for async functions. r=arai

Keywords: checkin-needed

== Change summary for alert #19689 (as of Wed, 27 Feb 2019 19:20:17 GMT) ==

Improvements:

1% Base Content JS osx-10-10 opt stylo 4,077,512.00 -> 4,032,372.00
1% Base Content JS windows10-64-pgo-qr opt stylo 4,132,861.33 -> 4,092,798.67

For up to date results, see: https://treeherder.mozilla.org/perf.html#/alerts?id=19689

Regressions: 1539782
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: