xref: /aosp_15_r20/external/kotlinx.coroutines/kotlinx-coroutines-debug/src/DebugProbes.kt (revision 7a7160fed73afa6648ef8aa100d4a336fe921d9a)
1 @file:Suppress("UNUSED", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
2 
3 package kotlinx.coroutines.debug
4 
5 import kotlinx.coroutines.*
6 import kotlinx.coroutines.debug.internal.*
7 import java.io.*
8 import java.lang.management.*
9 import kotlin.coroutines.*
10 
11 /**
12  * Kotlin debug probes support.
13  *
14  * Debug probes is a dynamic attach mechanism which installs multiple hooks into coroutines machinery.
15  * It slows down all coroutine-related code, but in return provides diagnostic information, including
16  * asynchronous stacktraces, coroutine dumps (similar to [ThreadMXBean.dumpAllThreads] and `jstack`) via [DebugProbes.dumpCoroutines],
17  * and programmatic introspection of all alive coroutines.
18  * All introspecting methods throw [IllegalStateException] if debug probes were not installed.
19  *
20  * ### Consistency guarantees
21  *
22  * All snapshotting operations (e.g. [dumpCoroutines]) are *weakly-consistent*, meaning that they happen
23  * concurrently with coroutines progressing their own state. These operations are guaranteed to observe
24  * each coroutine's state exactly once, but the state is not guaranteed to be the most recent before the operation.
25  * In practice, it means that for snapshotting operations in progress, for each concurrent coroutine either
26  * the state prior to the operation or the state that was reached during the current operation is observed.
27  *
28  * ### Overhead
29  *
30  *  - Every created coroutine is stored in a concurrent hash map, and the hash map is looked up in and
31  *    updated on each suspension and resumption.
32  *  - If [DebugProbes.enableCreationStackTraces] is enabled, stack trace of the current thread is captured on
33  *    each created coroutine that is a rough equivalent of throwing an exception per each created coroutine.
34  *
35  * ### Internal machinery and classloading.
36  *
37  * Under the hood, debug probes replace internal `kotlin.coroutines.jvm.internal.DebugProbesKt` class that has the following
38  * empty static methods:
39  *
40  * - `probeCoroutineResumed` that is invoked on every [Continuation.resume].
41  * - `probeCoroutineSuspended` that is invoked on every continuation suspension.
42  * - `probeCoroutineCreated` that is invoked on every coroutine creation.
43  *
44  * with a `kotlinx-coroutines`-specific class to keep track of all the coroutines machinery.
45  *
46  * The new class is located in the `kotlinx-coroutines-core` module, meaning that all target application classes that use
47  * coroutines and `suspend` functions have to be loaded by the classloader in which `kotlinx-coroutines-core` classes are available.
48  */
49 @ExperimentalCoroutinesApi
50 public object DebugProbes {
51 
52     /**
53      * Whether coroutine creation stack traces should be sanitized.
54      * Sanitization removes all frames from `kotlinx.coroutines` package except
55      * the first one and the last one to simplify diagnostic.
56      *
57      * `true` by default.
58      */
59     public var sanitizeStackTraces: Boolean
60         get() = DebugProbesImpl.sanitizeStackTraces
61         @Suppress("INVISIBLE_SETTER") // do not remove the INVISIBLE_SETTER suppression: required in k2
62         set(value) {
63             DebugProbesImpl.sanitizeStackTraces = value
64         }
65 
66     /**
67      * Whether coroutine creation stack traces should be captured.
68      * When enabled, for each created coroutine a stack trace of the current thread is captured and attached to the coroutine.
69      * This option can be useful during local debug sessions, but is recommended
70      * to be disabled in production environments to avoid performance overhead of capturing real stacktraces.
71      *
72      * `false` by default.
73      */
74     public var enableCreationStackTraces: Boolean
75         get() = DebugProbesImpl.enableCreationStackTraces
76         @Suppress("INVISIBLE_SETTER") // do not remove the INVISIBLE_SETTER suppression: required in k2
77         set(value) {
78             DebugProbesImpl.enableCreationStackTraces = value
79         }
80 
81     /**
82      * Whether to ignore coroutines whose context is [EmptyCoroutineContext].
83      *
84      * Coroutines with empty context are considered to be irrelevant for the concurrent coroutines' observability:
85      * - They do not contribute to any concurrent executions
86      * - They do not contribute to the (concurrent) system's liveness and/or deadlocks, as no other coroutines might wait for them
87      * - The typical usage of such coroutines is a combinator/builder/lookahead parser that can be debugged using more convenient tools.
88      *
89      * `true` by default.
90      */
91     public var ignoreCoroutinesWithEmptyContext: Boolean
92         get() = DebugProbesImpl.ignoreCoroutinesWithEmptyContext
93         @Suppress("INVISIBLE_SETTER") // do not remove the INVISIBLE_SETTER suppression: required in k2
94         set(value) {
95             DebugProbesImpl.ignoreCoroutinesWithEmptyContext = value
96         }
97 
98     /**
99      * Determines whether debug probes were [installed][DebugProbes.install].
100      */
101     public val isInstalled: Boolean get() = DebugProbesImpl.isInstalled
102 
103     /**
104      * Installs a [DebugProbes] instead of no-op stdlib probes by redefining
105      * debug probes class using the same class loader as one loaded [DebugProbes] class.
106      */
installnull107     public fun install() {
108         DebugProbesImpl.install()
109     }
110 
111     /**
112      * Uninstall debug probes.
113      */
uninstallnull114     public fun uninstall() {
115         DebugProbesImpl.uninstall()
116     }
117 
118     /**
119      * Invokes given block of code with installed debug probes and uninstall probes in the end.
120      */
withDebugProbesnull121     public inline fun withDebugProbes(block: () -> Unit) {
122         install()
123         try {
124             block()
125         } finally {
126             uninstall()
127         }
128     }
129 
130     /**
131      * Returns string representation of the coroutines [job] hierarchy with additional debug information.
132      * Hierarchy is printed from the [job] as a root transitively to all children.
133      */
jobToStringnull134     public fun jobToString(job: Job): String = DebugProbesImpl.hierarchyToString(job)
135 
136     /**
137      * Returns string representation of all coroutines launched within the given [scope].
138      * Throws [IllegalStateException] if the scope has no a job in it.
139      */
140     public fun scopeToString(scope: CoroutineScope): String =
141         jobToString(scope.coroutineContext[Job] ?: error("Job is not present in the scope"))
142 
143     /**
144      * Prints [job] hierarchy representation from [jobToString] to the given [out].
145      */
146     public fun printJob(job: Job, out: PrintStream = System.out): Unit =
147         out.println(DebugProbesImpl.hierarchyToString(job))
148 
149     /**
150      * Prints all coroutines launched within the given [scope].
151      * Throws [IllegalStateException] if the scope has no a job in it.
152      */
153     public fun printScope(scope: CoroutineScope, out: PrintStream = System.out): Unit =
154         printJob(scope.coroutineContext[Job] ?: error("Job is not present in the scope"), out)
155 
156     /**
157      * Returns all existing coroutines' info.
158      * The resulting collection represents a consistent snapshot of all existing coroutines at the moment of invocation.
159      */
160     public fun dumpCoroutinesInfo(): List<CoroutineInfo> =
161         DebugProbesImpl.dumpCoroutinesInfo().map { CoroutineInfo(it) }
162 
163     /**
164      * Dumps all active coroutines into the given output stream, providing a consistent snapshot of all existing coroutines at the moment of invocation.
165      * The output of this method is similar to `jstack` or a full thread dump. It can be used as the replacement to
166      * "Dump threads" action.
167      *
168      * Example of the output:
169      * ```
170      * Coroutines dump 2018/11/12 19:45:14
171      *
172      * Coroutine "coroutine#42":StandaloneCoroutine{Active}@58fdd99, state: SUSPENDED
173      *     at MyClass$awaitData.invokeSuspend(MyClass.kt:37)
174      *     at _COROUTINE._CREATION._(CoroutineDebugging.kt)
175      *     at MyClass.createIoRequest(MyClass.kt:142)
176      *     at MyClass.fetchData(MyClass.kt:154)
177      *     at MyClass.showData(MyClass.kt:31)
178      * ...
179      * ```
180      */
dumpCoroutinesnull181     public fun dumpCoroutines(out: PrintStream = System.out): Unit = DebugProbesImpl.dumpCoroutines(out)
182 }
183