Skip to content

Commit c7239ac

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 c7239ac

File tree

30 files changed

+1500
-21
lines changed

30 files changed

+1500
-21
lines changed

README.md

Lines changed: 2 additions & 0 deletions
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+
* [debug](core/README.md) — debug 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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 39 additions & 0 deletions
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

Lines changed: 4 additions & 1 deletion
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.**
Lines changed: 9 additions & 0 deletions
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

Lines changed: 18 additions & 12 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 3 additions & 1 deletion
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

0 commit comments

Comments
 (0)