[vm] Document the new implementation of async/async*/sync* based on suspend/resume stubs

TEST=none (documentation only change)

Issue: https://github.com/dart-lang/sdk/issues/48378
Change-Id: I61e734f85c40e3a301c4185e8917e78c37171590
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/251680
Reviewed-by: Ryan Macnak <rmacnak@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Alexander Markov <alexmarkov@google.com>
This commit is contained in:
Alexander Markov 2022-07-21 16:18:23 +00:00 committed by Commit Bot
parent 5741f11f7b
commit 1f48a6c7dd

549
runtime/docs/async.md Normal file
View file

@ -0,0 +1,549 @@
# Suspendable Functions (`async`, `async*` and `sync*`)
This document describes the implementation of _suspendable_ functions (functions with `async`,
`async*` or `sync*` modifier) in Dart VM. The execution of such functions can be suspended in
the middle at `await`/`yield`/`yield*` and resumed afterwards.
When suspending a function, its local execution state (local variables and temporaries) is saved
and the control is returned to the caller of the suspended function.
When resuming a function, its local execution state is restored and execution continues within
the suspendable function from the point where it was suspended.
In order to minimize code size, the implementation is built using a variety of _stubs_ - reusable
snippets of machine code generated by the VM/AOT.
The high-level Dart logic used to implement suspendable functions (such as managing
Futures/Streams/Iterators) is factored into helper Dart methods in core library.
The rest of the document is organized as follows: first, general mechanisms for implementation of
suspendable functions are described.
After that, `async`, `async*` and `sync*` implementations are outlined using the general
mechanisms introduced before.
# Building blocks common to all suspendable functions
## SuspendState objects
SuspendState objects are allocated on the heap and encapsulate the saved state of a suspended
function. When suspending a function, its local frame (including local variables, spill slots
and expression stack) is copied from the stack to a SuspendState object on the heap.
When resuming a function, the frame is recreated and copied back from the SuspendState object
into the stack.
SuspendState objects have variable size and keep frame in the "payload" following a few fixed
fields.
In addition to a stack frame, SuspendState records a PC in the code of the suspended function
where execution was suspended and can be resumed.
The PC is also used by GC to find a stack map and scan through the pointers in the copied frame.
SuspendState object also holds data and callbacks specific to a particular kind of suspendable
function.
SuspendState object is allocated during the first suspension and can be reused for the subsequent
suspensions of the same function.
For the declaration of SuspendState see [object.h](https://github.com/dart-lang/sdk/blob/main/runtime/vm/object.h#:~:text=class%20SuspendState),
UntaggedSuspendState is declared in [raw_object.h](https://github.com/dart-lang/sdk/blob/main/runtime/vm/raw_object.h#:~:text=class%20UntaggedSuspendState).
There is also a corresponding Dart class `_SuspendState`, declared in [async_patch.dart](https://github.com/dart-lang/sdk/blob/main/sdk/lib/_internal/vm/lib/async_patch.dart#:~:text=class%20_SuspendState).
It contains Dart methods which are used to customize implementation for a particular kind of
suspendable function.
## Frame of a suspendable function
Suspendable functions are never inlined into other functions, so their local state is not mixed
with the state of their callers (but other functions may be inlined into them).
In order to have a single contiguous region of memory to copy during suspend/resume, parameters of
suspendable functions are always copied into the local frame in the function prologue (see uses of
`Function::MakesCopyOfParameters()` predicate).
In order to keep and reuse SuspendState object, each suspendable function has an artificial local
variable `:suspend_state` (see uses of `ParsedFunction::suspend_state_var()`), which is always
allocated at the fixed offset in frame. It occupies the first local variable slot
(`SuspendState::kSuspendStateVarIndex`) in case of unoptimized code or the first spill slot
in case of optimized code (see `FlowGraphAllocator::AllocateSpillSlotForSuspendState`).
The fixed location helps to find this variable in various stubs and runtime.
## Prologue and InitSuspendableFunction stub
At the very beginning of a suspendable function `null` is stored into `:suspend_state` variable.
This guarantees that `:suspend_state` variable can be accessed any time by GC and exception
handling.
After checking bounds of type arguments and types of arguments, suspendable functions call
InitSuspendableFunction stub.
InitSuspendableFunction stub does the following:
- It calls a static generic Dart method specific to a particular kind of suspendable function.
The argument of the stub is passed as type arguments to that method.
Dart method performs initialization specific to a particular kind of suspendable function
(for example, it creates `_Future<T>()` for async functions).
It returns the instance which is used as a function-specific data.
- Stub puts the function-specific data to `:suspend_state` variable, where it can be found by
Suspend or Return stubs later.
## Suspend stub
Suspend stub is called from a suspendable function when its execution should be suspended.
Suspend stub does the following:
- It inspects `:suspend_state` variable and checks if it contains an instance of SuspendState.
If it doesn't, then stub allocates a new instance with a payload sufficient to hold a frame of
the suspendable function. The newly allocated SuspendState is stored into `:suspend_state`
variable, and previous value of `:suspend_state` (coming from InitSuspendableFunction stub) is
saved to `SuspendState.function_data`.
- In JIT mode, size of the frame may vary over time - expression stack depth varies during
execution of unoptimized code and frame size may change during deoptimization and OSR.
In AOT mode size of the stack frame stays the same.
So, if stub finds an existing SuspendState object in JIT mode, it also checks if its frame
payload has a sufficient size to hold a frame of the suspendable function. If it is not
large enough, suspend stub calls `AllocateSuspendState` runtime entry to allocate a larger
SuspendState object. The same runtime entry is called for slow path when allocating
SuspendState for the first time.
- The return address from Suspend stub to the suspendable function is saved to `SuspendState.pc`.
It will be used to resume execution later.
- The contents of the stack frame of the suspendable function between FP and SP is copied into
SuspendState.
- Write barrier: if SuspendState object resides in the old generation, then
EnsureRememberedAndMarkingDeferred runtime entry is called.
- If implementation of particular kind of suspendable function uses a customized Dart method
for the suspension, then that method is called.
Suspend stub supports passing one argument to the customization method.
The result of the method is returned back to the caller of the suspendable function - it's
the result of the suspendable function.
If such method is not used, then Suspend stub returns its argument (so suspendable function
could customize its return value).
- On architectures other than x64/ia32, the frame of the suspendable function is removed and
stub returns directly to the caller of the suspendable function.
On x64/ia32, in order to maintain call/return balance and avoid performance penalty,
Suspend stub returns to the suspendable function which immediately returns to its caller.
For more details see `StubCodeCompiler::GenerateSuspendStub` in [stub_code_compiler.cc](https://github.com/dart-lang/sdk/blob/main/runtime/vm/compiler/stub_code_compiler.cc#:~:text=StubCodeCompiler::GenerateSuspendStub).
## Resume stub
Resume stub is tail-called from `_SuspendState._resume` recognized method (which is called
from Dart helpers). It is used to resume execution of the previously suspended function.
Resume stub does the following:
- Allocates Dart frame on the stack, using `SuspendState.frame_size` to calculate its size.
- Copies frame contents from SuspendState to the stack.
- In JIT mode restores pool pointer (PP).
- Checks for the following cases and calls ResumeFrame runtime entry if any of this is true:
+ If resuming with an exception.
+ In JIT mode, if Code of the suspendable function is disabled (deoptimized).
+ In JIT mode, if there is a resumption breakpoint set by debugger.
- Otherwise, jumps to `SuspendState.pc` to resume execution of the suspended function.
On x64/ia32 the continuation PC is adjusted by adding `SuspendStubABI::kResumePcDistance`
to skip over the epilogue which immediately follows the Suspend stub call to maintain
call/return balance.
ResumeFrame runtime entry is called as if it was called from suspended function at continuation PC.
It handles all corner cases by throwing an exception, lazy deoptimizing or calling into
the debugger.
For more details see `StubCodeCompiler::GenerateResumeStub` in [stub_code_compiler.cc](https://github.com/dart-lang/sdk/blob/main/runtime/vm/compiler/stub_code_compiler.cc#:~:text=StubCodeCompiler::GenerateResumeStub)
and `ResumeFrame` in [runtime_entry.cc](https://github.com/dart-lang/sdk/blob/main/runtime/vm/runtime_entry.cc#:~:text=ResumeFrame).
## Return stub
Suspendable functions can use Return stub if they need to do something when execution of
a function ends (for example, complete a Future or close a Stream). In such a case,
suspendable function jumps to the Return stub instead of returning.
Return stub does the following:
- Removes the frame of the suspendable function (as if function epilogue was executed).
- Calls a Dart method specific to a particular kind of suspendable function.
The customization method takes a value of `:suspend_state` variable and a return value
passed from the body of the suspendable function to the stub.
- The value returned from the customization method is used as the result of
the suspendable function.
For more details see `StubCodeCompiler::GenerateReturnStub` in [stub_code_compiler.cc](https://github.com/dart-lang/sdk/blob/main/runtime/vm/compiler/stub_code_compiler.cc#:~:text=StubCodeCompiler::GenerateReturnStub).
## Exception handling and AsyncExceptionHandler stub
Certain kinds of suspendable functions (async and async*) may need to catch all thrown exceptions
which are not caught within the function body, and perform certain actions (such as completing
the Future with an error).
This is implemented by setting `has_async_handler` bit on `ExceptionHandlers` object.
When looking for an exception handler, runtime checks if this bit is set and uses
AsyncExceptionHandler stub as a handler (see `StackFrame::FindExceptionHandler`).
AsyncExceptionHandler stub does the following:
- It inspects the value of `:suspend_state` variable. If it is `null` (meaning the prologue has not
finished yet), the exception should not be handled and it is rethrown.
This makes it possible for argument type checks to throw an exception synchronously
instead of completing a Future with an error.
- Otherwise, stub removes the frame of the suspendable function (as if function epilogue was
executed) and calls `_SuspendState._handleException` Dart method. AsyncExceptionHandler stub
does not use separate Dart helper methods for async and async* functions as exception handling is
not performance sensitive and currently uses only one bit in `ExceptionHandlers` to select
a stub handler for simplicity.
- The value returned from `_SuspendState._handleException` is used as the result of the
suspendable function.
For more details see `StubCodeCompiler::GenerateAsyncExceptionHandlerStub` in [stub_code_compiler.cc](https://github.com/dart-lang/sdk/blob/main/runtime/vm/compiler/stub_code_compiler.cc#:~:text=StubCodeCompiler::GenerateAsyncExceptionHandlerStub).
## IL instructions
When compiling suspendable functions, the following IL instructions are used:
- `Call1ArgStub` instruction is used to call one-argument stubs such as InitSuspendableFunction.
- `Suspend` instruction is used to call Suspend stub. After calling Suspend stub,
on x64/ia32 it also generates an epilogue right after the stub, in order to
return to the caller after suspending without disrupting call/return balance.
Due to this extra epilogue following the Suspend stub call, the resumption PC is
not the same as the return address of the Suspend stub. So `Suspend` instruction
uses 2 distinct deopt ids for the Suspend stub call and resumption PC.
- `Return` instruction jumps to a Return stub instead of returning for certain kinds
of suspendable functions (async and async*).
# Combining all pieces together
## Async functions
See [async_patch.dart](https://github.com/dart-lang/sdk/blob/main/sdk/lib/_internal/vm/lib/async_patch.dart) for the corresponding Dart source code.
Async functions use the following customized stubs:
### InitAsync stub
InitAsync = InitSuspendableFunction stub which calls `_SuspendState._initAsync`.
`_SuspendState._initAsync` creates a `_Future<T>` instance which is used as the result of
the async function. This `_Future<T>` instance is kept in `:suspend_state` variable until
`_SuspendState` instance is created during the first `await`, and then kept in
`_SuspendState._functionData`. This instance is returned from `_SuspendState._await`,
`_SuspendState._returnAsync`, `_SuspendState._returnAsyncNotFuture` and
`_SuspendState._handleException` methods to serve as the result of the async function.
### Await stub
Await = Suspend stub which calls `_SuspendState._await`. It implements the `await` expression.
`_SuspendState._await` allocates 'then' and 'error' callback closures when called for
the first time. These callback closures resume execution of the async function via Resume stub.
It is possible to create callbacks eagerly in the InitAsync stub, but there is a significant
fraction of async functions which don't have `await` at all, so creating callbacks lazily during
the first `await` makes those functions more efficient.
If an argument of `await` is a Future, then `_SuspendState._await` attaches 'then' and 'error'
callbacks to that Future. Otherwise it schedules a micro-task to continue execution of
the suspended function later.
### ReturnAsync stub
ReturnAsync stub = Return stub which calls `_SuspendState._returnAsync`.
It is used to implement `return` statement (either explicit or implicit when reaching
the end of function).
`_SuspendState._returnAsync` completes `_Future<T>` which is used as the result of
the async function.
### ReturnAsyncNotFuture stub
ReturnAsyncNotFuture stub = Return stub which calls `_SuspendState._returnAsyncNotFuture`.
ReturnAsyncNotFuture is similar to ReturnAsync, but used when compiler can prove that
return value is not a Future. It bypasses the expensive `is Future` test.
### Execution flow in async functions
The following diagram depicts how the control is passed in a typical async function:
```
Caller Future<T> foo() async Stubs Dart _SuspendState methods
|
*-------------------> |
(prologue) -------------> InitAsync
|
*----------> _initAsync
(creates _Future<T>)
| <---------
| <-----------------------*
|
|
(await) ----------------> AwaitAsync
|
*----------> _await
(setups resumption)
(returns _Future<T>)
| <---------
| <---------------------------------------------*
Awaited Future is completed
|
*------------------------------------------> Resume
|
(after await) <---------------*
|
|
(return) ---------------> ReturnAsync/ReturnAsyncNotFuture
|
*----------> _returnAsync/_returnAsyncNotFuture
(completes _Future<T>)
(returns _Future<T>)
| <---------
| <---------------------------------------------*
```
## Async* functions
See [async_patch.dart](https://github.com/dart-lang/sdk/blob/main/sdk/lib/_internal/vm/lib/async_patch.dart)
for the corresponding Dart source code.
Async* functions use the following customized stubs:
### InitAsyncStar stub
InitAsyncStar = InitSuspendableFunction stub which calls `_SuspendState._initAsyncStar`.
`_SuspendState._initAsyncStar` creates `_AsyncStarStreamController<T>` instance which is used
to control the Stream returned from the async* function. `_AsyncStarStreamController<T>` is kept
in `_SuspendState._functionData` (after the first suspension at the beginning of async* function).
## YieldAsyncStar stub and `yield`/`yield*`
YieldAsyncStar = Suspend stub which calls `_SuspendState._yieldAsyncStar`.
This stub is used to suspend async* function at the beginning (until listener is attached to
the Stream returned from async* function), and at `yield` / `yield*` statements.
When `_SuspendState._yieldAsyncStar` is called at the beginning of async* function it creates
a callback closure to resume body of the async* function (via Resume stub), creates and
returns `Stream`.
`yield` / `yield*` statements are implemented in the following way:
```
_AsyncStarStreamController controller = :suspend_state._functionData;
if (controller.add/addStream(<expr>)) {
return;
}
if (YieldAsyncStar()) {
return;
}
```
`_AsyncStarStreamController.add`, `_AsyncStarStreamController.addStream` and YieldAsyncStar stub
can return `true` to indicate that Stream doesn't have a listener anymore and execution of
async* function should end.
Note that YieldAsyncStar stub returns a value passed to a Resume stub when resuming async*
function, so the 2nd hasListeners check happens right before the async* function is resumed.
See `StreamingFlowGraphBuilder::BuildYieldStatement` for more details about `yield` / `yield*`.
### Await stub
Async* functions use the same Await stub which is used by async functions.
### ReturnAsyncStar stub
ReturnAsyncStar stub = Return stub which calls `_SuspendState._returnAsyncStar`.
`_SuspendState._returnAsyncStar` closes the Stream.
### Execution flow in async* functions
The following diagram depicts how the control is passed in a typical async* function:
```
Caller Stream<T> foo() async* Stubs Dart helper methods
|
*-------------------> |
(prologue) -------------> InitAsyncStar
|
*----------> _SuspendState._initAsyncStar
(creates _AsyncStarStreamController<T>)
| <---------
| <-----------------------*
* ------------------> YieldAsyncStar
|
*----------> _SuspendState._yieldAsyncStar
(setups resumption)
(returns _AsyncStarStreamController.stream)
| <---------
| <---------------------------------------------*
Stream is listened
|
*------------------------------------------> Resume
|
(after prologue) <--------------*
|
|
(yield) --------------------------------> _AsyncStarStreamController.add
(adds value to Stream)
(checks if there are listeners)
| <-----------------------------------
* ------------------> YieldAsyncStar
|
*----------> _SuspendState._yieldAsyncStar
| <---------
| <---------------------------------------------*
Micro-task to run async* body
|
*----------------------------------------------------------> _AsyncStarStreamController.runBody
(checks if there are listeners)
Resume <-------
|
(after yield) <---------------*
|
|
(return) ---------------> ReturnAsyncStar
|
*----------> _SuspendState._returnAsyncStar
(closes _AsyncStarStreamController)
| <---------
| <---------------------------------------------*
```
## Sync* functions
See [async_patch.dart](https://github.com/dart-lang/sdk/blob/main/sdk/lib/_internal/vm/lib/async_patch.dart)
for the corresponding Dart source code.
Sync* functions use the following customized stubs:
### InitSyncStar stub
InitSyncStar = InitSuspendableFunction stub which calls `_SuspendState._initSyncStar`.
`_SuspendState._initSyncStar` creates a `_SyncStarIterable<T>` instance which is returned
from sync* function.
### SuspendSyncStarAtStart stub
SuspendSyncStarAtStart = Suspend stub which calls `_SuspendState._suspendSyncStarAtStart`.
This stub is used to suspend execution of sync* at the beginning. It is called after
InitSyncStar in the sync* function prologue. The body of sync* function doesn't run
until Iterator is not obtained from Iterable (`_SyncStarIterable<T>`) which is returned from
the sync* function.
### CloneSuspendState stub
This stub creates a copy of SuspendState object. It is used to clone state of sync*
function (suspended at the beginning) for each Iterator instance obtained from
Iterable.
See `StubCodeCompiler::GenerateCloneSuspendStateStub`.
### SuspendSyncStarAtYield stub and `yield`/`yield*`
SuspendSyncStarAtYield = Suspend stub which doesn't call helper Dart methods.
SuspendSyncStarAtYield is used to implement `yield` / `yield*` statements in sync* functions.
`yield` / `yield*` statements are implemented in the following way:
```
_SyncStarIterator iterator = :suspend_state._functionData;
iterator._current = <expr>; // yield <expr>
OR
iterator._yieldStarIterable = <expr>; // yield* <expr>
SuspendSyncStarAtYield(true);
```
See `StreamingFlowGraphBuilder::BuildYieldStatement` for more details about `yield` / `yield*`.
The value passed to SuspendSyncStarAtYield is returned back from the invocation of
Resume stub. `true` indicates that iteration can continue.
### Returning from sync* functions.
Sync* function do not use Return stubs. Instead, return statements are rewritten to return `false`
in order to indicate that iteration is finished.
### Execution flow in sync* functions
The following diagram depicts how the control is passed in a typical sync* function:
```
Caller Iterable<T> foo() sync* Stubs Dart helpers
|
*-------------------> |
(prologue) -------------> InitSyncStar
|
*----------> _SuspendState._initSyncStar
(creates _SyncStarIterable<T>)
| <---------
| <-----------------------*
* ------------------> SuspendSyncStarAtStart
|
*----------> _SuspendState._suspendSyncStarAtStart
(remembers _SuspendState at start)
(returns _SyncStarIterable<T>)
| <---------
| <---------------------------------------------*
Iterable.iterator is called
|
*----------------------------------------------------------> _SyncStarIterable<T>.iterator
(creates _SyncStarIterator<T>)
|
CloneSuspendState <-------*
(makes a copy of _SuspendState at start)
|
*-----------> |
| <------------------------------------------------------- (returns _SyncStarIterator<T>)
Iterator.moveNext is called
|
*----------------------------------------------------------> _SyncStarIterator<T>.moveNext
(iterates over the cached yield* iterator, if any)
(resumes sync* body to get the next element)
Resume <-------
|
(after prologue) <--------------*
|
|
(yield) ---------------> SuspendSyncStarAtYield(true)
|
*---------->
(the next element is cached in _SyncStarIterator<T>._current)
(returns true indicating that the next element is available)
| <----------------------------------------------------------
Iterator.moveNext is called
|
*----------------------------------------------------------> _SyncStarIterator<T>.moveNext
(iterates over the cached yield* iterator, if any)
(resumes sync* body to get the next element)
Resume <-------
|
(after yield) <-----------------*
|
|
(return false) ----------------------------->
(returns false indicating that iteration is finished)
| <----------------------------------------------------------
```