-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Refactor await and yield implementation and enable async iteration #6312
Conversation
It's nice to see the horrible OP_ResumeYield logic replaced. A little sad to see that much of my implementation of AsyncGenerators is removed/re-written here BUT I can see that this way of doing it is a lot neater. NOTE: I have not reviewed this, I've just had a general read of the code. A couple of things stand out to me by way of future improvements:
|
Hi @rhuanjl - I was a bit nervous about posting this PR given there were so many changes to what you had done, which was awesome work given the existing generators implementation. But particularly with JITing coming I want to take this opportunity to clean up the existing oddness. Apologies!
Oh right, I had forgotten about that, and it was a good idea. Hopefully this change will make it a bit easier to apply the same optimization to async functions (since there are fewer continuation functions to worry about).
Yes! I was trying to figure out whether it would be better to store it in a C++ field or create a register for it in bytecode, but I didn't try to implement it either way yet.
Right. My idea was to go ahead and take care of the bytecode part here, and then use your existing PR for the remaining changes to the library code. I think I got all of the bytecode here? |
From talking about it thought I'd have a stab at it, commits available for cherry-picking if wanted though I really just did this for fun so won't mind if they're not used:
On the Iterator Next point it does indeed look like you hit all of the Bytecode changes for it; so yeah just a case of taking my existing PR and dropping the bytecode changes and fixing the conflicts on JavascriptOperators.cpp and should fully fix the issue. |
Those changes look great! We can bring them in after this one gets landed. For the TTD stuff, I don't really know it either but I think @boingoing could help when we get there. I'm also trying to consider what we want to do with TTD longer-term. |
Awesome!! 😊😊 |
Ooh! Will that fix #4630, I wonder...? 🤞 |
It doesn't - BUT it lays the groundwork for an easy fix of #4630 that I have ready to submit after this lands (if someone doesn't beat me to it). |
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.
It looks good to me. I like the cleanups in ptree.h, bytecode generator, and splitting of async code from JavascriptPromise. Couple questions but overall I think it seems right.
@@ -7322,7 +7322,6 @@ void Parser::ParseExpressionLambdaBody(ParseNodeFnc * pnodeLambda, bool fAllowIn | |||
// Pushing a statement node with PushStmt<>() normally does this initialization | |||
// but do it here manually since we know there is no outer statement node. | |||
pnodeRet->grfnop = 0; | |||
pnodeRet->pnodeOuter = nullptr; |
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.
Is this a drive-by cleanup of pnodeOuter because it isn't used?
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.
It was only used for the EmitJumpCleanup
routine in the byte code generator. After joining the "jump cleanup" paths with "generator yield cleanup" paths, we don't need this state in the parse tree anymore.
@@ -863,21 +859,8 @@ class ParseNodeJump : public ParseNodeStmt | |||
DISABLE_SELF_CAST(ParseNodeJump); | |||
}; | |||
|
|||
// base for loop nodes | |||
class ParseNodeLoop : public ParseNodeStmt |
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.
Wow. Hard to believe this hasn't been cleaned-up before.
// (that is when the function is called). This yield opcode is to mark the begining of the function body. | ||
// TODO: Inserting a yield should have almost no impact on perf as it is a direct return from the function. But this needs | ||
// to be verified. Ideally if the function has simple parameter list then we can avoid inserting the opcode and the additional call. | ||
if (pnodeFnc->IsGenerator()) |
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.
Nice
void EmitIteratorComplete(Js::RegSlot doneLocation, Js::RegSlot iteratorResultLocation, ByteCodeGenerator* byteCodeGenerator, FuncInfo* funcInfo); | ||
void EmitIteratorValue(Js::RegSlot valueLocation, Js::RegSlot iteratorResultLocation, ByteCodeGenerator* byteCodeGenerator, FuncInfo* funcInfo); | ||
|
||
void EmitFunctionCall( |
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.
Are these functions just moved up here?
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.
There was quite a bit of code movement here in order to better support calling iterator methods from bytecode. For example, we needed to cache the "next" method of iterators and use that method on subsequent calls in for-of and yield star. To that end, EmitIteratorNext
and EmitIteratorValue
were removed in favor of the more general EmitFunctionCall
and EmitGetObjectProperty
.
{ | ||
byteCodeGenerator->tryScopeRecordsList.UnlinkFromEnd(); | ||
} | ||
byteCodeGenerator->PopJumpCleanup(); |
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 like the cleanups related to this list - good stuff.
{ | ||
// On resuming from a yield, we branch based on the ResumeYieldKind | ||
// integer value | ||
byteCodeGenerator->EnregisterConstant((uint)Js::ResumeYieldKind::Normal); |
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'm a little confused how this constant is used. We're sticking it in the constant table. Is this pulled-out when we're doing the resume? Is there other generator state here or is it usually on the stack frame?
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 asking about this.
These constants are used to determine how the generator was resumed. If the generator was resumed with a "normal completion", then the resume object will have a kind property whose value is (uint)Js::ResumeYieldKind::Normal
. (Similarly for "throw" and "return".) Bytecode is generated that checks the "kind" property against these constant values.
Do you think this is a good approach?
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 the point of registering these constants is just to pass the values so we can compare them to some other value in a register or such when executing the bytecode?
// 4. Let queue be generator.[[AsyncGeneratorQueue]]. | ||
// 5. Let request be AsyncGeneratorRequest { [[Completion]]: completion, [[Capability]]: promiseCapability }. | ||
AsyncGeneratorRequest* request = RecyclerNew(scriptContext->GetRecycler(), AsyncGeneratorRequest, input, exceptionObj, promise); | ||
// TODO: BUGBUG - figure out how to determine what the prototype was |
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.
Is this ok? Is TTD broken with this?
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.
@boingoing I believe TDD has always been broken on this path. I remember seeing this same comment but didn't have enough context to understand it further.
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.
Yes. All of this, including the TTD stuff (which I've got my eye on for possible simplification) was just copied over.
c94da69
to
b186ded
Compare
Note that the *_ReuseLoc variants should not be used the first time a code sequence writes to a register, only the 2nd and subsequent times. So: The reason is that _ReuseLoc tells the JIT not to start a new lifetime. So in the above example, each _ReuseLoc op extends an existing lifetime of R3. |
Thanks, @pleath I was wondering about that. I'll update with a fix. |
b186ded
to
1fb2a00
Compare
1fb2a00
to
c65dd32
Compare
@zenparsing do you think you'll be able to update this so we can land it? Or should I pull it down, update for the comments above and open a new PR? |
@zenparsing Please could you rebase this and regen the bytecode? I'll give it another review - I think I'm familiar enough now with most of this logic. (I've got some follow up PRs I want to open) |
Refactor await and yield implementation and enable async iteration See #6312 Co-authored-by: Kevin Smith <Kevin.Smith@microsoft.com>
Merged as c848d4d |
Overview
The goals of this change are to:
The primary architectural change is to perform yield resume logic and branching directly in bytecode, rather than relying on the
ResumeYield
helper, and to desugar all async functions and async generators to normal generators. This allows us to eliminate the special-caseGeneratorReturnException
exception type.In order to perform yield resume logic in bytecode, we resume the generator by creating a dynamic object with "value" (an arbitrary var) and "kind" (tagged int) fields. Branch instructions are inserted to deal with the various "kind" cases: "normal", "return", and "throw".
To desugar async functions and async generators to normal generators, we introduce a new type ID to represent "await results". Async generators then test for this type ID when determining whether their internal coroutine is "awaiting" or "yielding".
Changes
Architectural
ResumeYieldData
. Instead, resume the generator with a "resume yield object" containing "value" and "kind" properties.GeneratorReturnException
. The bytecode is now responsible for returning based on the resume yield kind.ResumeYield
,Await
,AsyncYield
,AsyncYieldStar
,AsyncYieldIsReturn
.NewAwaitObject
. This object type is yielded when awaiting and is uniquely distinguished by type ID from "normal" yield results.ResumeYield
andResumeYieldStar
opcodes and helpers. When generators are resumed, the resume yield object is loaded into the yield register and the resume logic is performed in bytecode.JavascriptGenerator
.ES2018AsyncIteration
by default.JIT
GeneratorResumeYieldLabel
.GeneratorLoadResumeYieldData
toGeneratorResumeYield
.ResumeYield
.GeneratorResumeYield
is inserted immediately after the bail-in instr and takes care of loading the resume yield object.ByteCode
EmitDummyYield
toEmitStartupYield
.tryScopeRecordsList
and loop "parent node linking" with a single mechanism for performing cleanup in both the branch-in-loop cases and yield cases.return
operand in async generators to match spec.yield *
inner iterator does not have a "throw" method.Parser
ParseNodeLoop
. The only purpose of this node type was to store an outer statement reference used when emitting jump cleanup. This information is now maintained in the bytecode generator'sjumpCleanupList
.Library
JavascriptAsyncGeneratorFunction
into a new file.JavascriptPromiseAsyncSpawnExecutorFunction
.CreateIteratorResultObject
functions.JavascriptAsyncFunction
into a new file (previously code was split betweenJavascriptPromise
andJavascriptGeneratorFunction
).JavascriptAsyncGeneratorFunction
into a new file.JavascriptGenerator::New
.Performance
This change results in a small performance increase on the ARES-6 benchmark.