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