Skip to content

Commit cf3c6e4

Browse files
committed
Introduce flag to disable creation stacktrace capturing in DebugProbes
1 parent a7cbad8 commit cf3c6e4

15 files changed

+210
-32
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ The `develop` branch is pushed to `master` during release.
277277
[ListenableFuture.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-guava/kotlinx.coroutines.guava/com.google.common.util.concurrent.-listenable-future/await.html
278278
<!--- MODULE kotlinx-coroutines-play-services -->
279279
<!--- INDEX kotlinx.coroutines.tasks -->
280+
[Task.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-play-services/kotlinx.coroutines.tasks/com.google.android.gms.tasks.-task/await.html
280281
<!--- MODULE kotlinx-coroutines-reactive -->
281282
<!--- INDEX kotlinx.coroutines.reactive -->
282283
[Publisher.collect]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-reactive/kotlinx.coroutines.reactive/org.reactivestreams.-publisher/collect.html

kotlinx-coroutines-debug/README.md

+20-3
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,20 @@ stacktraces will be dumped to the console.
5555

5656
### Using as JVM agent
5757

58-
It is possible to use this module as a standalone JVM agent to enable debug probes on the application startup.
58+
Debug module can also be used as a standalone JVM agent to enable debug probes on the application startup.
5959
You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.3.4.jar`.
6060
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.
61+
When used as Java agent, `"kotlinx.coroutines.debug.enable.creation.stack.trace"` system property can be used to control
62+
[DebugProbes.enableCreationStackTraces] along with agent startup.
63+
64+
### Using in production environment
65+
66+
It is possible to run an application in production environments with debug probes in order to monitor its
67+
state and improve its observability.
68+
For that, it is strongly recommended to switch off [DebugProbes.enableCreationStackTraces] property to significantly
69+
reduce the overhead of debug probes and make it insignificant.
70+
With creation stack-traces disabled, the typical overhead of enabled debug probes is a single-digit percentage of the total
71+
application throughput.
6172

6273

6374
### Example of usage
@@ -128,8 +139,13 @@ Dumping only deferred
128139

129140
### Status of the API
130141

131-
API is purely experimental and it is not guaranteed that it won't be changed (while it is marked as `@ExperimentalCoroutinesApi`).
132-
Do not use this module in production environment and do not rely on the format of the data produced by [DebugProbes].
142+
API is experimental, and it is not guaranteed it won't be changed (while it is marked as `@ExperimentalCoroutinesApi`).
143+
Like the rest of experimental API, `DebugProbes` is carefully designed, tested and ready to use in both test and production
144+
environments. It is marked as experimental to leave us the room to enrich the output data in a potentially backwards incompatible manner
145+
to further improve diagnostics and debugging experience.
146+
147+
The output format of [DebugProbes] can be changed in the future and it is not recommended to rely on the string representation
148+
of the dump programmatically.
133149

134150
### Debug agent and Android
135151

@@ -161,6 +177,7 @@ java.lang.NoClassDefFoundError: Failed resolution of: Ljava/lang/management/Mana
161177
[DebugProbes.dumpCoroutinesInfo]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/dump-coroutines-info.html
162178
[DebugProbes.printJob]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/print-job.html
163179
[DebugProbes.printScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/print-scope.html
180+
[DebugProbes.enableCreationStackTraces]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/enable-creation-stack-traces.html
164181
<!--- INDEX kotlinx.coroutines.debug.junit4 -->
165182
[CoroutinesTimeout]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug.junit4/-coroutines-timeout/index.html
166183
<!--- END -->

kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api

+14-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public final class kotlinx/coroutines/debug/DebugProbes {
1313
public final fun dumpCoroutines (Ljava/io/PrintStream;)V
1414
public static synthetic fun dumpCoroutines$default (Lkotlinx/coroutines/debug/DebugProbes;Ljava/io/PrintStream;ILjava/lang/Object;)V
1515
public final fun dumpCoroutinesInfo ()Ljava/util/List;
16+
public final fun getEnableCreationStackTraces ()Z
1617
public final fun getSanitizeStackTraces ()Z
1718
public final fun install ()V
1819
public final fun isInstalled ()Z
@@ -22,6 +23,7 @@ public final class kotlinx/coroutines/debug/DebugProbes {
2223
public final fun printScope (Lkotlinx/coroutines/CoroutineScope;Ljava/io/PrintStream;)V
2324
public static synthetic fun printScope$default (Lkotlinx/coroutines/debug/DebugProbes;Lkotlinx/coroutines/CoroutineScope;Ljava/io/PrintStream;ILjava/lang/Object;)V
2425
public final fun scopeToString (Lkotlinx/coroutines/CoroutineScope;)Ljava/lang/String;
26+
public final fun setEnableCreationStackTraces (Z)V
2527
public final fun setSanitizeStackTraces (Z)V
2628
public final fun uninstall ()V
2729
public final fun withDebugProbes (Lkotlin/jvm/functions/Function0;)V
@@ -35,17 +37,27 @@ public final class kotlinx/coroutines/debug/State : java/lang/Enum {
3537
public static fun values ()[Lkotlinx/coroutines/debug/State;
3638
}
3739

40+
public synthetic class kotlinx/coroutines/debug/internal/DebugProbesImplSequenceNumberRefVolatile {
41+
public fun <init> (J)V
42+
}
43+
3844
public final class kotlinx/coroutines/debug/junit4/CoroutinesTimeout : org/junit/rules/TestRule {
3945
public static final field Companion Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout$Companion;
4046
public fun <init> (JZ)V
4147
public synthetic fun <init> (JZILkotlin/jvm/internal/DefaultConstructorMarker;)V
48+
public fun <init> (JZZ)V
49+
public synthetic fun <init> (JZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
4250
public fun apply (Lorg/junit/runners/model/Statement;Lorg/junit/runner/Description;)Lorg/junit/runners/model/Statement;
4351
}
4452

4553
public final class kotlinx/coroutines/debug/junit4/CoroutinesTimeout$Companion {
54+
public final fun seconds (I)Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout;
4655
public final fun seconds (IZ)Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout;
56+
public final fun seconds (IZZ)Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout;
57+
public final fun seconds (J)Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout;
4758
public final fun seconds (JZ)Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout;
48-
public static synthetic fun seconds$default (Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout$Companion;IZILjava/lang/Object;)Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout;
49-
public static synthetic fun seconds$default (Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout$Companion;JZILjava/lang/Object;)Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout;
59+
public final fun seconds (JZZ)Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout;
60+
public static synthetic fun seconds$default (Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout$Companion;IZZILjava/lang/Object;)Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout;
61+
public static synthetic fun seconds$default (Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout$Companion;JZZILjava/lang/Object;)Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout;
5062
}
5163

kotlinx-coroutines-debug/src/AgentPremain.kt

+5
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@ import java.lang.instrument.*
1111
@Suppress("unused")
1212
internal object AgentPremain {
1313

14+
private val enableCreationStackTraces =
15+
System.getProperty("kotlinx.coroutines.debug.enable.creation.stack.trace")?.toBoolean()
16+
?: DebugProbes.enableCreationStackTraces
17+
1418
@JvmStatic
1519
public fun premain(args: String?, instrumentation: Instrumentation) {
1620
Installer.premain(args, instrumentation)
21+
DebugProbes.enableCreationStackTraces = enableCreationStackTraces
1722
DebugProbes.install()
1823
installSignalHandler()
1924
}

kotlinx-coroutines-debug/src/CoroutineInfo.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import kotlin.coroutines.jvm.internal.*
1616
@ExperimentalCoroutinesApi
1717
public class CoroutineInfo internal constructor(
1818
val context: CoroutineContext,
19-
private val creationStackBottom: CoroutineStackFrame,
19+
private val creationStackBottom: CoroutineStackFrame?,
2020
@JvmField internal val sequenceNumber: Long
2121
) {
2222

@@ -28,6 +28,7 @@ public class CoroutineInfo internal constructor(
2828

2929
/**
3030
* Creation stacktrace of the coroutine.
31+
* Can be empty if [DebugProbes.enableCreationStackTraces] is not set.
3132
*/
3233
public val creationStackTrace: List<StackTraceElement> get() = creationStackTrace()
3334

@@ -66,8 +67,9 @@ public class CoroutineInfo internal constructor(
6667
}
6768

6869
private fun creationStackTrace(): List<StackTraceElement> {
70+
val bottom = creationStackBottom ?: return emptyList()
6971
// Skip "Coroutine creation stacktrace" frame
70-
return sequence<StackTraceElement> { yieldFrames(creationStackBottom.callerFrame) }.toList()
72+
return sequence<StackTraceElement> { yieldFrames(bottom.callerFrame) }.toList()
7173
}
7274

7375
private tailrec suspend fun SequenceScope<StackTraceElement>.yieldFrames(frame: CoroutineStackFrame?) {

kotlinx-coroutines-debug/src/DebugProbes.kt

+14-4
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,30 @@ import kotlin.coroutines.*
2828
* * `probeCoroutineCreated` is invoked on every coroutine creation using stdlib intrinsics.
2929
*
3030
* Overhead:
31-
* * Every created coroutine is stored in a weak hash map, thus adding additional GC pressure.
32-
* * On every created coroutine, stacktrace of the current thread is dumped.
33-
* * On every `resume` and `suspend`, [WeakHashMap] is updated under a global lock.
31+
* * Every created coroutine is stored in a concurrent hash map and hash map is looked up and
32+
* updated on each suspension and resumption.
33+
* * If [DebugProbes.enableCreationStackTraces] is enabled, stack trace of the current thread is captured on
34+
* each created coroutine that is a rough equivalent of throwing an exception per each created coroutine.
3435
*/
3536
@ExperimentalCoroutinesApi
3637
public object DebugProbes {
3738

3839
/**
39-
* Whether coroutine creation stacktraces should be sanitized.
40+
* Whether coroutine creation stack traces should be sanitized.
4041
* Sanitization removes all frames from `kotlinx.coroutines` package except
4142
* the first one and the last one to simplify diagnostic.
4243
*/
4344
public var sanitizeStackTraces: Boolean = true
4445

46+
/**
47+
* Whether coroutine creation stack traces should be captured.
48+
* When enabled, for each created coroutine a stack trace of the current
49+
* thread is captured and attached to the coroutine.
50+
* This option can be useful during local debug sessions, but is recommended
51+
* to be disabled in production environments to avoid stack trace dumping overhead.
52+
*/
53+
public var enableCreationStackTraces: Boolean = true
54+
4555
/**
4656
* Determines whether debug probes were [installed][DebugProbes.install].
4757
*/

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

+20-9
Original file line numberDiff line numberDiff line change
@@ -334,18 +334,23 @@ internal object DebugProbesImpl {
334334
* even more verbose (it will attach coroutine creation stacktrace to all exceptions),
335335
* and then using CoroutineOwner completion as unique identifier of coroutineSuspended/resumed calls.
336336
*/
337-
val stacktrace = sanitizeStackTrace(Exception())
338-
val frame = stacktrace.foldRight<StackTraceElement, CoroutineStackFrame?>(null) { frame, acc ->
339-
object : CoroutineStackFrame {
340-
override val callerFrame: CoroutineStackFrame? = acc
341-
override fun getStackTraceElement(): StackTraceElement = frame
337+
338+
val frame = if (DebugProbes.enableCreationStackTraces) {
339+
val stacktrace = sanitizeStackTrace(Exception())
340+
stacktrace.foldRight<StackTraceElement, CoroutineStackFrame?>(null) { frame, acc ->
341+
object : CoroutineStackFrame {
342+
override val callerFrame: CoroutineStackFrame? = acc
343+
override fun getStackTraceElement(): StackTraceElement = frame
344+
}
342345
}
343-
}!!
346+
} else {
347+
null
348+
}
344349

345350
return createOwner(completion, frame)
346351
}
347352

348-
private fun <T> createOwner(completion: Continuation<T>, frame: CoroutineStackFrame): Continuation<T> {
353+
private fun <T> createOwner(completion: Continuation<T>, frame: CoroutineStackFrame?): Continuation<T> {
349354
if (!isInstalled) return completion
350355
val info = CoroutineInfo(completion.context, frame, sequenceNumber.incrementAndGet())
351356
val owner = CoroutineOwner(completion, info, frame)
@@ -372,8 +377,14 @@ internal object DebugProbesImpl {
372377
private class CoroutineOwner<T>(
373378
@JvmField val delegate: Continuation<T>,
374379
@JvmField val info: CoroutineInfo,
375-
frame: CoroutineStackFrame
376-
) : Continuation<T> by delegate, CoroutineStackFrame by frame {
380+
private val frame: CoroutineStackFrame?
381+
) : Continuation<T> by delegate, CoroutineStackFrame {
382+
383+
override val callerFrame: CoroutineStackFrame?
384+
get() = frame?.callerFrame
385+
386+
override fun getStackTraceElement(): StackTraceElement? = frame?.getStackTraceElement()
387+
377388
override fun resumeWith(result: Result<T>) {
378389
probeCoroutineCompleted(this)
379390
delegate.resumeWith(result)

kotlinx-coroutines-debug/src/junit4/CoroutinesTimeout.kt

+27-5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import java.util.concurrent.*
1717
*
1818
* Additionally, this rule installs [DebugProbes] and dumps all coroutines at the moment of the timeout.
1919
* It may cancel coroutines on timeout if [cancelOnTimeout] set to `true`.
20+
* [enableCoroutineCreationStackTraces] controls the corresponding [DebugProbes.enableCreationStackTraces] property
21+
* and can be optionally disabled to speed-up tests if creation stack traces are not needed.
2022
*
2123
* Example of usage:
2224
* ```
@@ -35,30 +37,50 @@ import java.util.concurrent.*
3537
*/
3638
public class CoroutinesTimeout(
3739
private val testTimeoutMs: Long,
38-
private val cancelOnTimeout: Boolean = false
40+
private val cancelOnTimeout: Boolean = false,
41+
private val enableCoroutineCreationStackTraces: Boolean = true
3942
) : TestRule {
4043

44+
@Suppress("UNUSED") // Binary compatibility
45+
constructor(testTimeoutMs: Long, cancelOnTimeout: Boolean = false) : this(testTimeoutMs, cancelOnTimeout, true)
46+
4147
init {
4248
require(testTimeoutMs > 0) { "Expected positive test timeout, but had $testTimeoutMs" }
4349
/*
4450
* Install probes in the constructor, so all the coroutines launched from within
4551
* target test constructor will be captured
4652
*/
53+
// Do not preserve previous state for unit-test environment
54+
DebugProbes.enableCreationStackTraces = enableCoroutineCreationStackTraces
4755
DebugProbes.install()
4856
}
4957

5058
companion object {
5159
/**
5260
* Creates [CoroutinesTimeout] rule with the given timeout in seconds.
5361
*/
54-
public fun seconds(seconds: Int, cancelOnTimeout: Boolean = false): CoroutinesTimeout =
55-
seconds(seconds.toLong(), cancelOnTimeout)
62+
@JvmOverloads
63+
public fun seconds(
64+
seconds: Int,
65+
cancelOnTimeout: Boolean = false,
66+
enableCoroutineCreationStackTraces: Boolean = true
67+
): CoroutinesTimeout =
68+
seconds(seconds.toLong(), cancelOnTimeout, enableCoroutineCreationStackTraces)
5669

5770
/**
5871
* Creates [CoroutinesTimeout] rule with the given timeout in seconds.
5972
*/
60-
public fun seconds(seconds: Long, cancelOnTimeout: Boolean = false): CoroutinesTimeout =
61-
CoroutinesTimeout(TimeUnit.SECONDS.toMillis(seconds), cancelOnTimeout) // Overflow is properly handled by TimeUnit
73+
@JvmOverloads
74+
public fun seconds(
75+
seconds: Long,
76+
cancelOnTimeout: Boolean = false,
77+
enableCoroutineCreationStackTraces: Boolean = true
78+
): CoroutinesTimeout =
79+
CoroutinesTimeout(
80+
TimeUnit.SECONDS.toMillis(seconds), // Overflow is properly handled by TimeUnit
81+
cancelOnTimeout,
82+
enableCoroutineCreationStackTraces
83+
)
6284
}
6385

6486
/**

kotlinx-coroutines-debug/test/DebugTestBase.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ open class DebugTestBase : TestBase() {
1818
open fun setUp() {
1919
before()
2020
DebugProbes.sanitizeStackTraces = false
21+
DebugProbes.enableCreationStackTraces = true
2122
DebugProbes.install()
2223
}
2324

@@ -29,4 +30,4 @@ open class DebugTestBase : TestBase() {
2930
onCompletion()
3031
}
3132
}
32-
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
package kotlinx.coroutines.debug
5+
6+
import kotlinx.coroutines.*
7+
import org.junit.*
8+
import org.junit.Test
9+
import kotlin.test.*
10+
11+
class DumpWithoutCreationStackTraceTest : DebugTestBase() {
12+
@Before
13+
override fun setUp() {
14+
super.setUp()
15+
DebugProbes.enableCreationStackTraces = false
16+
}
17+
18+
@Test
19+
fun testCoroutinesDump() = runTest {
20+
val deferred = createActiveDeferred()
21+
yield()
22+
verifyDump(
23+
"Coroutine \"coroutine#1\":BlockingCoroutine{Active}@70d1cb56, state: RUNNING\n" +
24+
"\tat java.lang.Thread.getStackTrace(Thread.java:1559)\n" +
25+
"\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.enhanceStackTraceWithThreadDump(DebugProbesImpl.kt:188)\n" +
26+
"\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.dumpCoroutinesSynchronized(DebugProbesImpl.kt:153)\n" +
27+
"\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.dumpCoroutines(DebugProbesImpl.kt:141)",
28+
29+
"Coroutine \"coroutine#2\":DeferredCoroutine{Active}@383fa309, state: SUSPENDED\n" +
30+
"\tat kotlinx.coroutines.debug.DumpWithoutCreationStackTraceTest\$createActiveDeferred\$1.invokeSuspend(DumpWithoutCreationStackTraceTest.kt:63)"
31+
)
32+
deferred.cancelAndJoin()
33+
}
34+
35+
36+
private fun CoroutineScope.createActiveDeferred(): Deferred<*> = async {
37+
suspendingMethod()
38+
assertTrue(true)
39+
}
40+
41+
private suspend fun suspendingMethod() {
42+
delay(Long.MAX_VALUE)
43+
}
44+
}

kotlinx-coroutines-debug/test/StracktraceUtils.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,14 @@ public fun verifyDump(vararg traces: String, ignoredCoroutine: String? = null) {
8080

8181
val expected = traces[index - 1].applyBackspace().split("\n\t(Coroutine creation stacktrace)\n", limit = 2)
8282
val actual = value.applyBackspace().split("\n\t(Coroutine creation stacktrace)\n", limit = 2)
83-
assertEquals(expected.size, actual.size)
83+
assertEquals(expected.size, actual.size, "Creation stacktrace should be part of the expected input")
8484

8585
expected.withIndex().forEach { (index, trace) ->
8686
val actualTrace = actual[index].trimStackTrace().sanitizeAddresses()
8787
val expectedTrace = trace.trimStackTrace().sanitizeAddresses()
8888
val actualLines = actualTrace.split("\n")
8989
val expectedLines = expectedTrace.split("\n")
90-
for (i in 0 until expectedLines.size) {
90+
for (i in expectedLines.indices) {
9191
assertEquals(expectedLines[i], actualLines[i])
9292
}
9393
}

0 commit comments

Comments
 (0)