Document coroutines codegen: debug
This commit is contained in:
committed by
Ilmir Usmanov
parent
995062cb19
commit
1cdae75dc3
@@ -3212,4 +3212,55 @@ runs shutdown procedure.
|
||||
6. `BaseContinuationImpl.resumeWith` function runs shutdown procedure once again. The KNPE is thrown.
|
||||
|
||||
To fix the issue, callable references to suspend functions returning unboxed inline classes check for `COROUTINE_SUSPENDED` and only then
|
||||
box the return value.
|
||||
box the return value.
|
||||
|
||||
## Debug
|
||||
Debugging coroutines is not as straightforward as debugging the usual code. For example, since a suspend call can suspend, even a simple
|
||||
debugging event, like `step-over`, requires support from both codegen and debugger to be even possible. Another example is throwing an
|
||||
exception from a coroutine, which might result in different stack traces depending on whether the coroutine has suspended and then resumed
|
||||
or the suspension has never happened. If the coroutine has suspended and then resumed, the stack trace might not even have user code, only
|
||||
the library code, making debugging a colossal PITA.
|
||||
|
||||
This section explains how the codegen provides the debugger information it needs.
|
||||
|
||||
### Step-over
|
||||
Historically, the first debugging support in coroutines codegen was `step-over`. The call returns either a value or `COROUTINE_SUSPENDED`
|
||||
marker. So, the state-machine checks the result and either continues the execution or returns the marker. Thus, the state-machine builder
|
||||
generates two execution paths: for direct calls and suspend-resume. If the call returns a value, the `step-over` action runs as usual.
|
||||
However, if we want to support the suspend-resume path, some tricks from both the codegen and the debugger are needed.
|
||||
|
||||
First of all, the codegen puts additional `LINENUMBER` instruction before returning the `COROUTINE_SUSPENDED` marker. This way, when a user
|
||||
presses `step-over`, the debugger places a breakpoint at this `LINENUMBER` and removes it when the execution reaches either the next line
|
||||
(in case of the direct path) or the breakpoint. The line number for this fictitious `LINENUMBER` is the line number of the caller's header.
|
||||
We chose this number since we need line numbers to be different from function to function (there can be multiple suspended calls, waiting
|
||||
for `step-over` to finish) and not interfering with user code.
|
||||
|
||||
When the coroutine resumes, the breakpoint is hit again, since the codegen generates another `LINENUMBER` at the start of the function.
|
||||
|
||||
To summarize, the codegen generates `N+1` fictitious `LINENUMBER` instruction in the suspend-resume path, so the debugger can place a
|
||||
breakpoint on the line number and emulate `step-over` with a single breakpoint, which is hit twice.
|
||||
|
||||
### Debug Metadata
|
||||
The continuation-passing-style explained that when we resume a coroutine, its caller becomes `BaseContinuationImpl.resumeWith`. However,
|
||||
the user wants to see a coroutine that called the suspended one. Fortunately, the information about the caller can be obtained through the
|
||||
completion chain. The stacktrace, constructed using the chain, is called Async Stack Trace.
|
||||
|
||||
However, first, the codegen should provide the information. To do this, every continuation class has
|
||||
`@kotlin.coroutines.jvm.internal.DebugMetadata` annotation, which contains the following fields:
|
||||
1. The source file, class name, and method name, along with line numbers, are used to generate async stack trace elements.
|
||||
2. An array of line numbers is a map from the `label` value to the suspension point line number.
|
||||
3. Mapping from spilled variables to locals, which the debugger uses to show the values of the variables.
|
||||
|
||||
Both the debugger and `kotlinx.coroutines` use debug metadata to generate async stack traces.
|
||||
|
||||
Continuation's `toString` method uses the debug metadata to show the location where the coroutine is suspended.
|
||||
|
||||
Note that tail-call optimization removes continuations. So, there are gaps in the async stack trace. Additionally, only line numbers of
|
||||
suspension points are stored; thus, line numbers are not always accurate.
|
||||
|
||||
### Probes
|
||||
`kotlin.coroutines.jvm.internal` package contains probe functions replaced by the debugger to show current coroutines (in a broad sense)
|
||||
and their statuses: suspended or running.
|
||||
1. `probeCoroutineCreate` is invoked in `createCoroutine` function.
|
||||
2. `probeCoroutineResumed` is invoked in `BaseContinuationImpl.resumeWith` function.
|
||||
3. `probeCoroutineSuspended` call is generated by the codegen if `suspendCoroutineUninterceptedOrReturn` returns `COROUTINE_SUSPENDED`.
|
||||
|
||||
Reference in New Issue
Block a user