Skip to content

Commit a5a7cdb

Browse files
authored
Merge pull request #876 from Kotlin/debug-agent
Coroutines debug agent Fixes #772
2 parents f64bcde + 3781a82 commit a5a7cdb

33 files changed

+1655
-41
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+
* [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

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

build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ buildscript {
4040
classpath "com.moowork.gradle:gradle-node-plugin:$gradle_node_version"
4141

4242
// JMH plugins
43-
classpath "com.github.jengelman.gradle.plugins:shadow:2.0.2"
43+
classpath "com.github.jengelman.gradle.plugins:shadow:4.0.2"
4444
classpath "me.champeau.gradle:jmh-gradle-plugin:0.4.7"
4545
classpath "net.ltgt.gradle:gradle-apt-plugin:0.10"
4646
}

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

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

931931
// for nicer debugging
932932
public override fun toString(): String =
933-
"${nameString()}{${stateString(state)}}@$hexAddress"
933+
"${toDebugString()}@$hexAddress"
934+
935+
@InternalCoroutinesApi
936+
public fun toDebugString(): String = "${nameString()}{${stateString(state)}}"
934937

935938
/**
936939
* @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

+12-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,23 @@ private fun createStackTrace(continuation: CoroutineStackFrame): ArrayDeque<Stac
179179
return stack
180180
}
181181

182-
internal fun sanitize(element: StackTraceElement): StackTraceElement {
182+
/**
183+
* @suppress
184+
*/
185+
@InternalCoroutinesApi
186+
public fun sanitize(element: StackTraceElement): StackTraceElement {
183187
if (!element.className.contains('/')) {
184188
return element
185189
}
186190
// KT-28237: STE generated with debug metadata contains '/' as separators in FQN, while Java contains dots
187191
return StackTraceElement(element.className.replace('/', '.'), element.methodName, element.fileName, element.lineNumber)
188192
}
189-
internal fun artificialFrame(message: String) = java.lang.StackTraceElement("\b\b\b($message", "\b", "\b", -1)
193+
194+
/**
195+
* @suppress
196+
*/
197+
@InternalCoroutinesApi
198+
public fun artificialFrame(message: String) = java.lang.StackTraceElement("\b\b\b($message", "\b", "\b", -1)
190199
internal fun StackTraceElement.isArtificial() = className.startsWith("\b\b\b")
191200
private fun Array<StackTraceElement>.frameIndex(methodName: String) = indexOfFirst { methodName == it.className }
192201

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

0 commit comments

Comments
 (0)