Skip to content

Commit 52c149d

Browse files
committed
Merge scoped coroutines into one in agent representation to avoid misleading dumps
1 parent c7239ac commit 52c149d

14 files changed

+428
-62
lines changed

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

+6
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@ private fun createStackTrace(continuation: CoroutineStackFrame): ArrayDeque<Stac
179179
return stack
180180
}
181181

182+
/**
183+
* @suppress
184+
*/
182185
@InternalCoroutinesApi
183186
public fun sanitize(element: StackTraceElement): StackTraceElement {
184187
if (!element.className.contains('/')) {
@@ -188,6 +191,9 @@ public fun sanitize(element: StackTraceElement): StackTraceElement {
188191
return StackTraceElement(element.className.replace('/', '.'), element.methodName, element.fileName, element.lineNumber)
189192
}
190193

194+
/**
195+
* @suppress
196+
*/
191197
@InternalCoroutinesApi
192198
public fun artificialFrame(message: String) = java.lang.StackTraceElement("\b\b\b($message", "\b", "\b", -1)
193199
internal fun StackTraceElement.isArtificial() = className.startsWith("\b\b\b")

core/kotlinx-coroutines-debug/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Debugging facilities for `kotlinx.coroutines` on JVM.
44

55
### Overview
6+
67
This module provides a debug JVM agent which allows to track and trace alive coroutines.
78
Main entry point to debug facilities is [DebugProbes].
89
Call to [DebugProbes.install] installs debug agent via ByteBuddy and starts to spy on coroutines when they are created, suspended or resumed.
@@ -13,6 +14,7 @@ Additionally, it is possible to process list of such coroutines via [DebugProbes
1314
of coroutines hierarchies referenced by [Job] instance using [DebugProbes.printHierarchy].
1415

1516
### Using as JVM agent
17+
1618
Additionally, it is possible to use this module as standalone JVM agent to enable debug probes on the application startup.
1719
You can run your application with additional argument: `-javaagent:kotlinx-coroutines-debug-1.1.0.jar`.
1820
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.

core/kotlinx-coroutines-debug/build.gradle

-3
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,5 @@ jar {
1111
manifest {
1212
attributes "Premain-Class": "kotlinx.coroutines.debug.AgentPremain"
1313
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(" ")
1714
}
1815
}

core/kotlinx-coroutines-debug/src/debug/AgentPremain.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ internal object AgentPremain {
1919
}
2020

2121
private fun installSignalHandler() {
22-
val signal = Signal("TRAP") // kill -5
23-
Signal.handle(signal, { DebugProbes.dumpCoroutines() })
22+
Signal.handle(Signal("TRAP") ) { // kill -5
23+
DebugProbes.dumpCoroutines()
24+
}
2425
}
2526
}

core/kotlinx-coroutines-debug/src/debug/CoroutineState.kt

+5-5
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import kotlin.coroutines.*
1717
public data class CoroutineState internal constructor(
1818
public val continuation: Continuation<*>,
1919
public val creationStackTrace: List<StackTraceElement>,
20-
internal val sequenceNumber: Long) {
20+
@JvmField internal val sequenceNumber: Long
21+
) {
2122

2223
/**
2324
* [Job] associated with a current coroutine or [IllegalStateException] otherwise.
@@ -37,7 +38,8 @@ public data class CoroutineState internal constructor(
3738
public val state: State get() = _state
3839

3940
// Copy constructor
40-
internal constructor(coroutine: Continuation<*>, state: CoroutineState) : this(coroutine,
41+
internal constructor(coroutine: Continuation<*>, state: CoroutineState) : this(
42+
coroutine,
4143
state.creationStackTrace,
4244
state.sequenceNumber) {
4345
_state = state.state
@@ -49,9 +51,7 @@ public data class CoroutineState internal constructor(
4951
private var lastObservedFrame: CoroutineStackFrame? = null
5052

5153
internal fun updateState(state: State, frame: Continuation<*>) {
52-
if (_state == state && lastObservedFrame != null) {
53-
return
54-
}
54+
if (_state == state && lastObservedFrame != null) return
5555

5656
_state = state
5757
lastObservedFrame = frame as? CoroutineStackFrame

core/kotlinx-coroutines-debug/src/debug/DebugProbes.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ import kotlin.coroutines.*
2727
* * `probeCoroutineCreated` is invoked on every coroutine creation using stdlib intrinsics.
2828
*
2929
* Overhead:
30-
* * Every created continuation is stored in a weak hash map, thus adding additional GC pressure.
31-
* * On every created continuation, stacktrace of the current thread is dumped.
30+
* * Every created coroutine is stored in a weak hash map, thus adding additional GC pressure.
31+
* * On every created coroutine, stacktrace of the current thread is dumped.
3232
* * On every `resume` and `suspend`, [WeakHashMap] is updated under a global lock.
3333
*
3434
* **WARNING: DO NOT USE DEBUG PROBES IN PRODUCTION ENVIRONMENT.**

core/kotlinx-coroutines-debug/src/debug/internal/DebugProbesImpl.kt

+19-11
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,10 @@ internal object DebugProbesImpl {
8484
@Suppress("DEPRECATION_ERROR")
8585
val str = if (this !is JobSupport) toString() else toDebugString()
8686
if (state == null) {
87-
builder.append("Coroutine: $str\n")
87+
@Suppress("INVISIBLE_REFERENCE")
88+
if (this !is ScopeCoroutine<*>) { // Do not print scoped coroutines
89+
builder.append("$str\n")
90+
}
8891
} else {
8992
val element = state.lastObservedStackTrace().firstOrNull()
9093
val contState = state.state
@@ -169,21 +172,25 @@ internal object DebugProbesImpl {
169172
coroutineState.updateState(state, frame)
170173
}
171174

172-
private fun Continuation<*>.owner(): ArtificialStackFrame<*>? {
173-
var frame = this as? CoroutineStackFrame ?: return null
174-
while (true) {
175-
if (frame is ArtificialStackFrame<*>) return frame
176-
val completion = frame.callerFrame ?: return null
177-
frame = completion
178-
}
179-
}
175+
private fun Continuation<*>.owner(): ArtificialStackFrame<*>? = (this as? CoroutineStackFrame)?.owner()
176+
177+
private tailrec fun CoroutineStackFrame.owner(): ArtificialStackFrame<*>? = if (this is ArtificialStackFrame<*>) this else callerFrame?.owner()
180178

181179
@Synchronized
182180
internal fun <T> probeCoroutineCreated(completion: Continuation<T>): Continuation<T> {
183181
if (!isInstalled) {
184182
return completion
185183
}
186184

185+
/*
186+
* If completion already has an owner, it means that we are in scoped coroutine (coroutineScope, withContext etc.),
187+
* then piggyback on its already existing owner and do not replace completion
188+
*/
189+
val owner = completion.owner()
190+
if (owner != null) {
191+
return completion
192+
}
193+
187194
/*
188195
* Here we replace completion with a sequence of CoroutineStackFrame objects
189196
* which represents creation stacktrace, thus making stacktrace recovery mechanism
@@ -211,8 +218,9 @@ internal object DebugProbesImpl {
211218
capturedCoroutines.remove(coroutine)
212219
}
213220

214-
private class ArtificialStackFrame<T>(val delegate: Continuation<T>, frame: CoroutineStackFrame) :
215-
Continuation<T> by delegate, CoroutineStackFrame by frame {
221+
private class ArtificialStackFrame<T>(
222+
@JvmField val delegate: Continuation<T>,
223+
frame: CoroutineStackFrame) : Continuation<T> by delegate, CoroutineStackFrame by frame {
216224

217225
override fun resumeWith(result: Result<T>) {
218226
probeCoroutineCompleted(this)

core/kotlinx-coroutines-debug/test/debug/CoroutinesDumpTest.kt

+15-15
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ package kotlinx.coroutines.debug
77
import kotlinx.coroutines.*
88
import org.junit.*
99
import org.junit.Test
10-
import java.io.*
1110
import kotlin.coroutines.*
1211
import kotlin.test.*
1312

@@ -88,20 +87,21 @@ class CoroutinesDumpTest : TestBase() {
8887

8988
awaitCoroutineStarted()
9089
verifyDump(
91-
"Coroutine \"coroutine#1\":DeferredCoroutine{Active}@1e4a7dd4, state: RUNNING (Last suspension stacktrace, not an actual stacktrace)\n" +
92-
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.nestedActiveMethod(CoroutinesDumpTest.kt:111)\n" +
93-
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.activeMethod(CoroutinesDumpTest.kt:106)\n" +
94-
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest\$testRunningCoroutineWithSuspensionPoint\$1\$deferred\$1.invokeSuspend(CoroutinesDumpTest.kt:71)\n" +
95-
"\t(Coroutine creation stacktrace)\n" +
96-
"\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" +
97-
"\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" +
98-
"\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n" +
99-
"\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:148)\n" +
100-
"\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt)\n" +
101-
"\tat kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" +
102-
"\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt)\n" +
103-
"\tat kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" +
104-
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.testRunningCoroutineWithSuspensionPoint(CoroutinesDumpTest.kt:71)")
90+
"Coroutine \"coroutine#1\":DeferredCoroutine{Active}@1e4a7dd4, state: RUNNING (Last suspension stacktrace, not an actual stacktrace)\n" +
91+
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.nestedActiveMethod(CoroutinesDumpTest.kt:111)\n" +
92+
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.activeMethod(CoroutinesDumpTest.kt:106)\n" +
93+
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest\$testRunningCoroutineWithSuspensionPoint\$1\$deferred\$1.invokeSuspend(CoroutinesDumpTest.kt:71)\n" +
94+
"\t(Coroutine creation stacktrace)\n" +
95+
"\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" +
96+
"\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" +
97+
"\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n" +
98+
"\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:148)\n" +
99+
"\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt)\n" +
100+
"\tat kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" +
101+
"\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt)\n" +
102+
"\tat kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" +
103+
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.testRunningCoroutineWithSuspensionPoint(CoroutinesDumpTest.kt:71)"
104+
)
105105
deferred.cancel()
106106
runBlocking { deferred.join() }
107107
}

core/kotlinx-coroutines-debug/test/debug/HierarchyToStringTest.kt

+43-19
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,45 @@ class HierarchyToStringTest : TestBase() {
3030
}
3131

3232
@Test
33-
fun testHierarchy() = runBlocking {
34-
val root = launch {
33+
fun testCompletingHierarchy() = runBlocking {
34+
val tab = '\t'
35+
val expectedString = """
36+
"coroutine#2":StandaloneCoroutine{Completing}
37+
$tab"foo#3":DeferredCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1${'$'}1.invokeSuspend(HierarchyToStringTest.kt:30)
38+
$tab"coroutine#4":ActorCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1${'$'}2${'$'}1.invokeSuspend(HierarchyToStringTest.kt:40)
39+
$tab$tab"coroutine#5":StandaloneCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1${'$'}2${'$'}job$1.invokeSuspend(HierarchyToStringTest.kt:37)
40+
""".trimIndent()
41+
42+
checkHierarchy(isCompleting = true, expectedString = expectedString)
43+
}
44+
45+
@Test
46+
fun testActiveHierarchy() = runBlocking {
47+
val tab = '\t'
48+
val expectedString = """
49+
"coroutine#2":StandaloneCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1.invokeSuspend(HierarchyToStringTest.kt:94)
50+
$tab"foo#3":DeferredCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1${'$'}1.invokeSuspend(HierarchyToStringTest.kt:30)
51+
$tab"coroutine#4":ActorCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1${'$'}2${'$'}1.invokeSuspend(HierarchyToStringTest.kt:40)
52+
$tab$tab"coroutine#5":StandaloneCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1${'$'}2${'$'}job$1.invokeSuspend(HierarchyToStringTest.kt:37)
53+
""".trimIndent()
54+
checkHierarchy(isCompleting = false, expectedString = expectedString)
55+
}
56+
57+
private suspend fun CoroutineScope.checkHierarchy(isCompleting: Boolean, expectedString: String) {
58+
val root = launchHierarchy(isCompleting)
59+
repeat(4) { yield() }
60+
expect(6)
61+
assertEquals(
62+
expectedString.trimStackTrace().trimPackage(),
63+
DebugProbes.hierarchyToString(root).trimEnd().trimStackTrace().trimPackage()
64+
)
65+
root.cancel()
66+
root.join()
67+
finish(7)
68+
}
69+
70+
private fun CoroutineScope.launchHierarchy(isCompleting: Boolean): Job {
71+
return launch {
3572
expect(1)
3673
async(CoroutineName("foo")) {
3774
expect(2)
@@ -50,24 +87,11 @@ class HierarchyToStringTest : TestBase() {
5087
job.join()
5188
}
5289
}
53-
}
54-
55-
repeat(4) { yield() }
56-
expect(6)
57-
val tab = '\t'
58-
val expectedString = """
59-
Coroutine: "coroutine#2":StandaloneCoroutine{Completing}
60-
$tab"foo#3":DeferredCoroutine{Active}, continuation is SUSPENDED at line kotlinx.coroutines.debug.HierarchyToStringTest${'$'}testHierarchy${'$'}1${'$'}root${'$'}1${'$'}1.invokeSuspend(HierarchyToStringTest.kt:30)
61-
$tab"coroutine#4":ActorCoroutine{Active}, continuation is SUSPENDED at line kotlinx.coroutines.debug.HierarchyToStringTest${'$'}testHierarchy${'$'}1${'$'}root${'$'}1${'$'}2.invokeSuspend(HierarchyToStringTest.kt:40)
62-
$tab$tab"coroutine#5":StandaloneCoroutine{Active}, continuation is SUSPENDED at line kotlinx.coroutines.debug.HierarchyToStringTest${'$'}testHierarchy${'$'}1${'$'}root${'$'}1${'$'}2${'$'}job$1.invokeSuspend(HierarchyToStringTest.kt:37)
63-
$tab$tab"coroutine#4":DispatchedCoroutine{Active}, continuation is SUSPENDED at line kotlinx.coroutines.debug.HierarchyToStringTest${'$'}testHierarchy${'$'}1${'$'}root${'$'}1${'$'}2${'$'}1.invokeSuspend(HierarchyToStringTest.kt:42)
64-
""".trimIndent()
6590

66-
// DebugProbes.printHierarchy(root) // <- use it for manual validation
67-
assertEquals(expectedString.trimStackTrace(), DebugProbes.hierarchyToString(root).trimEnd().trimStackTrace())
68-
root.cancel()
69-
root.join()
70-
finish(7)
91+
if (!isCompleting) {
92+
delay(Long.MAX_VALUE)
93+
}
94+
}
7195
}
7296

7397
private fun wrapperDispatcher(context: CoroutineContext): CoroutineContext {

core/kotlinx-coroutines-debug/test/debug/SanitizedProbesTest.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ import kotlinx.coroutines.*
99
import kotlinx.coroutines.debug.*
1010
import org.junit.*
1111
import org.junit.Test
12-
import java.io.*
1312
import java.util.concurrent.*
1413
import kotlin.test.*
1514

1615
class SanitizedProbesTest : TestBase() {
1716
@Before
1817
fun setUp() {
1918
before()
19+
DebugProbes.sanitizeStackTraces = true
2020
DebugProbes.install()
2121
}
2222

@@ -111,4 +111,4 @@ class SanitizedProbesTest : TestBase() {
111111
verifyStackTrace(e, traces)
112112
}
113113
}
114-
}
114+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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 kotlinx.coroutines.*
8+
import org.junit.*
9+
import kotlin.coroutines.*
10+
11+
class ScopedBuildersTest : TestBase() {
12+
@Before
13+
fun setUp() {
14+
before()
15+
DebugProbes.sanitizeStackTraces = false
16+
DebugProbes.install()
17+
}
18+
19+
@After
20+
fun tearDown() {
21+
try {
22+
DebugProbes.uninstall()
23+
} finally {
24+
onCompletion()
25+
}
26+
}
27+
28+
@Test
29+
fun testNestedScopes() = runBlocking {
30+
val job = launch { doInScope() }
31+
yield()
32+
yield()
33+
verifyDump(
34+
"Coroutine \"coroutine#1\":BlockingCoroutine{Active}@16612a51, state: RUNNING (Last suspension stacktrace, not an actual stacktrace)\n" +
35+
"\tat kotlinx.coroutines.debug.ScopedBuildersTest\$testNestedScopes\$1.invokeSuspend(ScopedBuildersTest.kt:32)\n" +
36+
"\t(Coroutine creation stacktrace)\n" +
37+
"\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n",
38+
39+
"Coroutine \"coroutine#2\":StandaloneCoroutine{Active}@6b53e23f, state: SUSPENDED\n" +
40+
"\tat kotlinx.coroutines.debug.ScopedBuildersTest\$doWithContext\$2.invokeSuspend(ScopedBuildersTest.kt:49)\n" +
41+
"\tat kotlinx.coroutines.debug.ScopedBuildersTest.doWithContext(ScopedBuildersTest.kt:47)\n" +
42+
"\tat kotlinx.coroutines.debug.ScopedBuildersTest\$doInScope\$2.invokeSuspend(ScopedBuildersTest.kt:41)\n" +
43+
"\tat kotlinx.coroutines.debug.ScopedBuildersTest\$testNestedScopes\$1\$job\$1.invokeSuspend(ScopedBuildersTest.kt:30)\n" +
44+
"\t(Coroutine creation stacktrace)\n" +
45+
"\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)")
46+
job.cancelAndJoin()
47+
finish(4)
48+
}
49+
50+
private suspend fun doInScope() = coroutineScope {
51+
expect(1)
52+
doWithContext()
53+
expectUnreached()
54+
}
55+
56+
private suspend fun doWithContext() {
57+
expect(2)
58+
withContext(wrapperDispatcher(coroutineContext)) {
59+
expect(3)
60+
delay(Long.MAX_VALUE)
61+
}
62+
expectUnreached()
63+
}
64+
}

0 commit comments

Comments
 (0)