Document coroutines codegen: debug

This commit is contained in:
Ilmir Usmanov
2020-09-08 18:15:02 +02:00
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`.