Skip to content

Commit 0bae0a7

Browse files
committed
Debug agent to track alive coroutines
* Can be installed dynamically or from command line * Captures coroutine creation stacktrace and stores it in completion, automatically enhancing stacktrace recovery mechanism * Allows to dump and introspect all active coroutines * Allows to dump Job hierarchy * When installed from command line, dumps all coroutines on kill -5 * Probe support in undispatched coroutines
1 parent 5a22d80 commit 0bae0a7

File tree

30 files changed

+1501
-21
lines changed

30 files changed

+1501
-21
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ GlobalScope.launch {
3131
* [core](core/README.md) — Kotlin/JVM implementation of common coroutines with additional features:
3232
* `Dispatchers.IO` dispatcher for blocking coroutines;
3333
* `Executor.asCoroutineDispatcher()` extension, custom thread pools, and more.
34+
* [core-test](core/README.md) — test utilities for coroutines.
35+
* `DebugProbes` API to probe, keep track of, print and dump active coroutines.
3436
* [js](js/README.md) — Kotlin/JS implementation of common coroutines with `Promise` support.
3537
* [native](native/README.md) — Kotlin/Native implementation of common coroutines with `runBlocking` single-threaded event loop.
3638
* [reactive](reactive/README.md) — modules that provide builders and iteration support for various reactive streams libraries:

RELEASE.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ To release new `<version>` of `kotlinx-coroutines`:
1212
`git merge origin/master`
1313

1414
4. Search & replace `<old-version>` with `<version>` across the project files. Should replace in:
15-
* [`README.md`](README.md)
15+
* [`README.md`](README.md) (native, core, test, debug, modules)
1616
* [`coroutines-guide.md`](docs/coroutines-guide.md)
1717
* [`gradle.properties`](gradle.properties)
1818
* [`ui/kotlinx-coroutines-android/example-app/gradle.properties`](ui/kotlinx-coroutines-android/example-app/gradle.properties)

binary-compatibility-validator/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies {
1313
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
1414

1515
testArtifacts project(':kotlinx-coroutines-core')
16+
testArtifacts project(':kotlinx-coroutines-debug')
1617

1718
testArtifacts project(':kotlinx-coroutines-reactive')
1819
testArtifacts project(':kotlinx-coroutines-reactor')

binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt

+1
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ public class kotlinx/coroutines/JobSupport : kotlinx/coroutines/ChildJob, kotlin
349349
public fun plus (Lkotlinx/coroutines/Job;)Lkotlinx/coroutines/Job;
350350
public final fun registerSelectClause0 (Lkotlinx/coroutines/selects/SelectInstance;Lkotlin/jvm/functions/Function1;)V
351351
public final fun start ()Z
352+
public final fun toDebugString ()Ljava/lang/String;
352353
public fun toString ()Ljava/lang/String;
353354
}
354355

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
public final class kotlinx/coroutines/debug/CoroutineState {
2+
public final fun component1 ()Lkotlin/coroutines/Continuation;
3+
public final fun component2 ()Ljava/util/List;
4+
public final fun copy (Lkotlin/coroutines/Continuation;Ljava/util/List;J)Lkotlinx/coroutines/debug/CoroutineState;
5+
public static synthetic fun copy$default (Lkotlinx/coroutines/debug/CoroutineState;Lkotlin/coroutines/Continuation;Ljava/util/List;JILjava/lang/Object;)Lkotlinx/coroutines/debug/CoroutineState;
6+
public fun equals (Ljava/lang/Object;)Z
7+
public final fun getContinuation ()Lkotlin/coroutines/Continuation;
8+
public final fun getCreationStackTrace ()Ljava/util/List;
9+
public final fun getJob ()Lkotlinx/coroutines/Job;
10+
public final fun getJobOrNull ()Lkotlinx/coroutines/Job;
11+
public final fun getState ()Lkotlinx/coroutines/debug/State;
12+
public fun hashCode ()I
13+
public final fun lastObservedStackTrace ()Ljava/util/List;
14+
public fun toString ()Ljava/lang/String;
15+
}
16+
17+
public final class kotlinx/coroutines/debug/DebugProbes {
18+
public static final field INSTANCE Lkotlinx/coroutines/debug/DebugProbes;
19+
public final fun dumpCoroutines (Ljava/io/PrintStream;)V
20+
public static synthetic fun dumpCoroutines$default (Lkotlinx/coroutines/debug/DebugProbes;Ljava/io/PrintStream;ILjava/lang/Object;)V
21+
public final fun dumpCoroutinesState ()Ljava/util/List;
22+
public final fun getSanitizeStackTraces ()Z
23+
public final fun hierarchyToString (Lkotlinx/coroutines/Job;)Ljava/lang/String;
24+
public final fun install ()V
25+
public final fun printHierarchy (Lkotlinx/coroutines/Job;Ljava/io/PrintStream;)V
26+
public static synthetic fun printHierarchy$default (Lkotlinx/coroutines/debug/DebugProbes;Lkotlinx/coroutines/Job;Ljava/io/PrintStream;ILjava/lang/Object;)V
27+
public final fun setSanitizeStackTraces (Z)V
28+
public final fun uninstall ()V
29+
public final fun withDebugProbes (Lkotlin/jvm/functions/Function0;)V
30+
}
31+
32+
public final class kotlinx/coroutines/debug/State : java/lang/Enum {
33+
public static final field CREATED Lkotlinx/coroutines/debug/State;
34+
public static final field RUNNING Lkotlinx/coroutines/debug/State;
35+
public static final field SUSPENDED Lkotlinx/coroutines/debug/State;
36+
public static fun valueOf (Ljava/lang/String;)Lkotlinx/coroutines/debug/State;
37+
public static fun values ()[Lkotlinx/coroutines/debug/State;
38+
}
39+

common/kotlinx-coroutines-core-common/src/JobSupport.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,10 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren
925925

926926
// for nicer debugging
927927
public override fun toString(): String =
928-
"${nameString()}{${stateString(state)}}@$hexAddress"
928+
"${toDebugString()}@$hexAddress"
929+
930+
@InternalCoroutinesApi
931+
public fun toDebugString(): String = "${nameString()}{${stateString(state)}}"
929932

930933
/**
931934
* @suppress **This is unstable API and it is subject to change.**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.internal
6+
7+
import kotlin.coroutines.*
8+
9+
internal expect inline fun <T> probeCoroutineCreated(completion: Continuation<T>): Continuation<T>

common/kotlinx-coroutines-core-common/src/intrinsics/Undispatched.kt

+18-12
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import kotlin.coroutines.intrinsics.*
1515
* It does not use [ContinuationInterceptor] and does not update context of the current thread.
1616
*/
1717
internal fun <T> (suspend () -> T).startCoroutineUnintercepted(completion: Continuation<T>) {
18-
startDirect(completion) {
19-
startCoroutineUninterceptedOrReturn(completion)
18+
startDirect(completion) { actualCompletion ->
19+
startCoroutineUninterceptedOrReturn(actualCompletion)
2020
}
2121
}
2222

@@ -26,8 +26,8 @@ internal fun <T> (suspend () -> T).startCoroutineUnintercepted(completion: Conti
2626
* It does not use [ContinuationInterceptor] and does not update context of the current thread.
2727
*/
2828
internal fun <R, T> (suspend (R) -> T).startCoroutineUnintercepted(receiver: R, completion: Continuation<T>) {
29-
startDirect(completion) {
30-
startCoroutineUninterceptedOrReturn(receiver, completion)
29+
startDirect(completion) { actualCompletion ->
30+
startCoroutineUninterceptedOrReturn(receiver, actualCompletion)
3131
}
3232
}
3333

@@ -37,9 +37,9 @@ internal fun <R, T> (suspend (R) -> T).startCoroutineUnintercepted(receiver: R,
3737
* It does not use [ContinuationInterceptor], but updates the context of the current thread for the new coroutine.
3838
*/
3939
internal fun <T> (suspend () -> T).startCoroutineUndispatched(completion: Continuation<T>) {
40-
startDirect(completion) {
40+
startDirect(completion) { actualCompletion ->
4141
withCoroutineContext(completion.context, null) {
42-
startCoroutineUninterceptedOrReturn(completion)
42+
startCoroutineUninterceptedOrReturn(actualCompletion)
4343
}
4444
}
4545
}
@@ -50,23 +50,29 @@ internal fun <T> (suspend () -> T).startCoroutineUndispatched(completion: Contin
5050
* It does not use [ContinuationInterceptor], but updates the context of the current thread for the new coroutine.
5151
*/
5252
internal fun <R, T> (suspend (R) -> T).startCoroutineUndispatched(receiver: R, completion: Continuation<T>) {
53-
startDirect(completion) {
53+
startDirect(completion) { actualCompletion ->
5454
withCoroutineContext(completion.context, null) {
55-
startCoroutineUninterceptedOrReturn(receiver, completion)
55+
startCoroutineUninterceptedOrReturn(receiver, actualCompletion)
5656
}
5757
}
5858
}
5959

60-
private inline fun <T> startDirect(completion: Continuation<T>, block: () -> Any?) {
60+
/**
61+
* Starts given [block] immediately in the current stack-frame until first suspension point.
62+
* This method supports debug probes and thus can intercept completion, thus completion is provide
63+
* as the parameter of [block].
64+
*/
65+
private inline fun <T> startDirect(completion: Continuation<T>, block: (Continuation<T>) -> Any?) {
66+
val actualCompletion = probeCoroutineCreated(completion)
6167
val value = try {
62-
block()
68+
block(actualCompletion)
6369
} catch (e: Throwable) {
64-
completion.resumeWithException(e)
70+
actualCompletion.resumeWithException(e)
6571
return
6672
}
6773
if (value !== COROUTINE_SUSPENDED) {
6874
@Suppress("UNCHECKED_CAST")
69-
completion.resume(value as T)
75+
actualCompletion.resume(value as T)
7076
}
7177
}
7278

core/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ Module name below corresponds to the artifact name in Maven/Gradle.
55

66
## Modules
77

8-
* [kotlinx-coroutines-core](kotlinx-coroutines-core/README.md) -- core coroutine builders and synchronization primitives.
9-
8+
* [kotlinx-coroutines-core](kotlinx-coroutines-core/README.md) &mdash; core coroutine builders and synchronization primitives.
9+
* [kotlinx-coroutines-debug](kotlinx-coroutines-debug/README.md) &mdash; coroutines debug utilities.

core/kotlinx-coroutines-core/src/Debug.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public const val DEBUG_PROPERTY_VALUE_ON = "on"
4141
*/
4242
public const val DEBUG_PROPERTY_VALUE_OFF = "off"
4343

44+
@JvmField
4445
internal val DEBUG = systemProp(DEBUG_PROPERTY_NAME).let { value ->
4546
when (value) {
4647
DEBUG_PROPERTY_VALUE_AUTO, null -> CoroutineId::class.java.desiredAssertionStatus()
@@ -50,7 +51,8 @@ internal val DEBUG = systemProp(DEBUG_PROPERTY_NAME).let { value ->
5051
}
5152
}
5253

53-
internal val RECOVER_STACKTRACE = systemProp(STACKTRACE_RECOVERY_PROPERTY_NAME, true)
54+
@JvmField
55+
internal val RECOVER_STACKTRACES = systemProp(STACKTRACE_RECOVERY_PROPERTY_NAME, true)
5456

5557
// internal debugging tools
5658

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
@file:Suppress("NOTHING_TO_INLINE", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
5+
6+
package kotlinx.coroutines.internal
7+
8+
import kotlin.coroutines.*
9+
import kotlin.coroutines.jvm.internal.probeCoroutineCreated as probe
10+
11+
internal actual inline fun <T> probeCoroutineCreated(completion: Continuation<T>): Continuation<T> = probe(completion)

core/kotlinx-coroutines-core/src/internal/StackTraceRecovery.kt

+6-3
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ internal actual fun <E : Throwable> unwrap(exception: E): E {
165165
}
166166

167167
private fun <E : Throwable> recoveryDisabled(exception: E) =
168-
!RECOVER_STACKTRACE || !DEBUG || exception is CancellationException || exception is NonRecoverableThrowable
168+
!RECOVER_STACKTRACES || !DEBUG || exception is CancellationException || exception is NonRecoverableThrowable
169169

170170
private fun createStackTrace(continuation: CoroutineStackFrame): ArrayDeque<StackTraceElement> {
171171
val stack = ArrayDeque<StackTraceElement>()
@@ -179,14 +179,17 @@ private fun createStackTrace(continuation: CoroutineStackFrame): ArrayDeque<Stac
179179
return stack
180180
}
181181

182-
internal fun sanitize(element: StackTraceElement): StackTraceElement {
182+
@InternalCoroutinesApi
183+
public fun sanitize(element: StackTraceElement): StackTraceElement {
183184
if (!element.className.contains('/')) {
184185
return element
185186
}
186187
// KT-28237: STE generated with debug metadata contains '/' as separators in FQN, while Java contains dots
187188
return StackTraceElement(element.className.replace('/', '.'), element.methodName, element.fileName, element.lineNumber)
188189
}
189-
internal fun artificialFrame(message: String) = java.lang.StackTraceElement("\b\b\b($message", "\b", "\b", -1)
190+
191+
@InternalCoroutinesApi
192+
public fun artificialFrame(message: String) = java.lang.StackTraceElement("\b\b\b($message", "\b", "\b", -1)
190193
internal fun StackTraceElement.isArtificial() = className.startsWith("\b\b\b")
191194
private fun Array<StackTraceElement>.frameIndex(methodName: String) = indexOfFirst { methodName == it.className }
192195

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Module kotlinx-coroutines-debug
2+
3+
Debugging facilities for `kotlinx.coroutines` on JVM.
4+
5+
### Overview
6+
This module provides a debug JVM agent which allows to track and trace alive coroutines.
7+
Main entry point to debug facilities is [DebugProbes].
8+
Call to [DebugProbes.install] installs debug agent via ByteBuddy and starts to spy on coroutines when they are created, suspended or resumed.
9+
10+
After that you can use [DebugProbes.dumpCoroutines] to print all active (suspended or running) coroutines, including their state, creation and
11+
suspension stacktraces.
12+
Additionally, it is possible to process list of such coroutines via [DebugProbes.dumpCoroutinesState] or dump isolated parts
13+
of coroutines hierarchies referenced by [Job] instance using [DebugProbes.printHierarchy].
14+
15+
### Using as JVM agent
16+
Additionally, it is possible to use this module as standalone JVM agent to enable debug probes on the application startup.
17+
You can run your application with additional argument: `-javaagent:kotlinx-coroutines-debug-1.1.0.jar`.
18+
Additionally, on Linux and Mac OS X you can use `kill -5 $pid` command in order to force your application to print all alive coroutines.
19+
20+
21+
### Example of usage
22+
23+
Capabilities of this module can be demonstrated by the following example:
24+
```kotlin
25+
class Computation {
26+
public fun computeValue(): Deferred<String> = GlobalScope.async {
27+
val firstPart = computeFirstPart()
28+
val secondPart = computeSecondPart()
29+
30+
combineResults(firstPart, secondPart)
31+
}
32+
33+
private suspend fun combineResults(firstPart: Deferred<String>, secondPart: Deferred<String>): String {
34+
return firstPart.await() + secondPart.await()
35+
}
36+
37+
38+
private suspend fun CoroutineScope.computeFirstPart() = async {
39+
delay(5000)
40+
"4"
41+
}
42+
43+
private suspend fun CoroutineScope.computeSecondPart() = async {
44+
delay(5000)
45+
"2"
46+
}
47+
}
48+
49+
fun main(args: Array<String>) = runBlocking {
50+
DebugProbes.install()
51+
val computation = Computation()
52+
val deferred = computation.computeValue()
53+
54+
// Delay for some time
55+
delay(1000)
56+
57+
DebugProbes.dumpCoroutines()
58+
59+
println("\nDumping only deferred")
60+
DebugProbes.printHierarchy(deferred)
61+
}
62+
```
63+
64+
Printed result will be:
65+
```
66+
Coroutines dump 2018/11/12 21:44:02
67+
68+
Coroutine "coroutine#2":DeferredCoroutine{Active}@1b26f7b2, state: SUSPENDED
69+
at kotlinx.coroutines.DeferredCoroutine.await$suspendImpl(Builders.common.kt:99)
70+
at Computation.combineResults(Example.kt:18)
71+
at Computation$computeValue$1.invokeSuspend(Example.kt:14)
72+
(Coroutine creation stacktrace)
73+
at kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)
74+
at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)
75+
at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109)
76+
at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:160)
77+
at kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:88)
78+
at kotlinx.coroutines.BuildersKt.async(Unknown Source)
79+
at kotlinx.coroutines.BuildersKt__Builders_commonKt.async$default(Builders.common.kt:81)
80+
at kotlinx.coroutines.BuildersKt.async$default(Unknown Source)
81+
at Computation.computeValue(Example.kt:10)
82+
at ExampleKt$main$1.invokeSuspend(Example.kt:36)
83+
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)
84+
at kotlinx.coroutines.DispatchedTask$DefaultImpls.run(Dispatched.kt:237)
85+
at kotlinx.coroutines.DispatchedContinuation.run(Dispatched.kt:81)
86+
at kotlinx.coroutines.EventLoopBase.processNextEvent(EventLoop.kt:123)
87+
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:69)
88+
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:45)
89+
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
90+
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:35)
91+
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
92+
at ExampleKt.main(Example.kt:33)
93+
94+
... More coroutines here ...
95+
96+
Dumping only deferred
97+
"coroutine#2":DeferredCoroutine{Active}, continuation is SUSPENDED at line kotlinx.coroutines.DeferredCoroutine.await$suspendImpl(Builders.common.kt:99)
98+
"coroutine#3":DeferredCoroutine{Active}, continuation is SUSPENDED at line Computation$computeFirstPart$2.invokeSuspend(Example.kt:23)
99+
"coroutine#4":DeferredCoroutine{Active}, continuation is SUSPENDED at line Computation$computeSecondPart$2.invokeSuspend(Example.kt:28)
100+
```
101+
102+
103+
### Status of the API
104+
105+
API is purely experimental and it is not guaranteed that it won't be changed (while it is marked as `@ExperimentalCoroutinesApi`).
106+
Do not use this module in production environment and do not rely on the format of the data produced by [DebugProbes].
107+
108+
<!--- MODULE kotlinx-coroutines-core -->
109+
<!--- INDEX kotlinx.coroutines -->
110+
[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
111+
<!--- MODULE kotlinx-coroutines-debug -->
112+
<!--- INDEX kotlinx.coroutines.debug -->
113+
[DebugProbes]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/index.html
114+
[DebugProbes.install]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/install.html
115+
[DebugProbes.dumpCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/dump-coroutines.html
116+
[DebugProbes.dumpCoroutinesState]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/dump-coroutines-state.html
117+
[DebugProbes.printHierarchy]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/print-hierarchy.html
118+
<!--- END -->
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
dependencies {
6+
compile "net.bytebuddy:byte-buddy:$byte_buddy_version"
7+
compile "net.bytebuddy:byte-buddy-agent:$byte_buddy_version"
8+
}
9+
10+
jar {
11+
manifest {
12+
attributes "Premain-Class": "kotlinx.coroutines.debug.AgentPremain"
13+
attributes "Can-Redefine-Classes": "true"
14+
// For local runs
15+
// attributes "Main-Class": "kotlinx.coroutines.debug.Playground"
16+
// attributes "Class-Path": configurations.compile.collect { it.absolutePath }.join(" ")
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.debug
6+
7+
import net.bytebuddy.agent.*
8+
import sun.misc.*
9+
import java.lang.instrument.*
10+
11+
@Suppress("unused")
12+
internal object AgentPremain {
13+
14+
@JvmStatic
15+
public fun premain(args: String?, instrumentation: Instrumentation) {
16+
Installer.premain(args, instrumentation)
17+
DebugProbes.install()
18+
installSignalHandler()
19+
}
20+
21+
private fun installSignalHandler() {
22+
val signal = Signal("TRAP") // kill -5
23+
Signal.handle(signal, { DebugProbes.dumpCoroutines() })
24+
}
25+
}

0 commit comments

Comments
 (0)