diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/coroutines-codegen.md b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/coroutines-codegen.md index 61e43334423..8541de739c2 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/coroutines-codegen.md +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/coroutines-codegen.md @@ -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. \ No newline at end of file +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`.