Skip to content

Commit 4116fbf

Browse files
authored
Make DebugProbes ready to production (#1862)
* Speed-up installed debug probes by splitting global probes lock to RW-lock, guard all state transitions with read lock and all read operations with write lock to guarantee a consistent snapshot * Prevent IllegalStateException during 'kill -5' command * Introduce flag to disable creation stacktrace capturing in DebugProbes * Support proposed changes in JUnit4 rules Fixes #1379 Fixes #1372
1 parent 10fd73e commit 4116fbf

15 files changed

+250
-59
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

+12-1
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,28 @@ 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
}
2025

2126
private fun installSignalHandler() {
2227
try {
2328
Signal.handle(Signal("TRAP")) { // kill -5
24-
DebugProbes.dumpCoroutines()
29+
if (DebugProbes.isInstalled) {
30+
// Case with 'isInstalled' changed between this check-and-act is not considered
31+
// a real debug probes use-case, thus is not guarded against.
32+
DebugProbes.dumpCoroutines()
33+
} else {
34+
println("""Cannot perform coroutines dump, debug probes are disabled""")
35+
}
2536
}
2637
} catch (t: Throwable) {
2738
System.err.println("Failed to install signal handler: $t")

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

+15-5
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
*/
@@ -132,5 +142,5 @@ public object DebugProbes {
132142
internal fun probeCoroutineResumed(frame: Continuation<*>) = DebugProbesImpl.probeCoroutineResumed(frame)
133143

134144
internal fun probeCoroutineSuspended(frame: Continuation<*>) = DebugProbesImpl.probeCoroutineSuspended(frame)
135-
internal fun <T> probeCoroutineCreated(completion: kotlin.coroutines.Continuation<T>): kotlin.coroutines.Continuation<T> =
145+
internal fun <T> probeCoroutineCreated(completion: Continuation<T>): Continuation<T> =
136146
DebugProbesImpl.probeCoroutineCreated(completion)

0 commit comments

Comments
 (0)