Skip to content

Commit d4f45b6

Browse files
qwwdfsadwhyolegdkhalanskyjb
authored
Do not track coroutines with empty coroutine context in DebugProbes (#3784)
Such coroutines typically are a subject for significant debugger overhead, can be observed with more conventional tools, and do not contribute to the state of the system that is typically observed with coroutines debugger Fixes #3782 Co-authored-by: Oleg Yukhnevich <[email protected]> Co-authored-by: Dmitry Khalanskiy <[email protected]>
1 parent 5664713 commit d4f45b6

File tree

6 files changed

+134
-37
lines changed

6 files changed

+134
-37
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package benchmarks.debug
@@ -9,46 +9,24 @@ import kotlinx.coroutines.debug.*
99
import org.openjdk.jmh.annotations.*
1010
import org.openjdk.jmh.annotations.State
1111
import java.util.concurrent.*
12+
import java.util.concurrent.atomic.AtomicInteger
1213

14+
/**
15+
* The benchmark is supposed to show the DebugProbes overhead for a non-concurrent sequence builder.
16+
* The code is actually part of the IDEA codebase, originally reported here: https://github.com/Kotlin/kotlinx.coroutines/issues/3527
17+
*/
1318
@Warmup(iterations = 5, time = 1)
1419
@Measurement(iterations = 5, time = 1)
1520
@Fork(value = 1)
1621
@BenchmarkMode(Mode.AverageTime)
1722
@OutputTimeUnit(TimeUnit.MICROSECONDS)
1823
@State(Scope.Benchmark)
19-
open class DebugProbesConcurrentBenchmark {
20-
21-
@Setup
22-
fun setup() {
23-
DebugProbes.sanitizeStackTraces = false
24-
DebugProbes.enableCreationStackTraces = false
25-
DebugProbes.install()
26-
}
27-
28-
@TearDown
29-
fun tearDown() {
30-
DebugProbes.uninstall()
31-
}
32-
24+
open class DebugSequenceOverheadBenchmark {
3325

34-
@Benchmark
35-
fun run() = runBlocking<Long> {
36-
var sum = 0L
37-
repeat(8) {
38-
launch(Dispatchers.Default) {
39-
val seq = stressSequenceBuilder((1..100).asSequence()) {
40-
(1..it).asSequence()
41-
}
42-
43-
for (i in seq) {
44-
sum += i.toLong()
45-
}
46-
}
47-
}
48-
sum
49-
}
50-
51-
private fun <Node> stressSequenceBuilder(initialSequence: Sequence<Node>, children: (Node) -> Sequence<Node>): Sequence<Node> {
26+
private fun <Node> generateRecursiveSequence(
27+
initialSequence: Sequence<Node>,
28+
children: (Node) -> Sequence<Node>
29+
): Sequence<Node> {
5230
return sequence {
5331
val initialIterator = initialSequence.iterator()
5432
if (!initialIterator.hasNext()) {
@@ -68,4 +46,45 @@ open class DebugProbesConcurrentBenchmark {
6846
}
6947
}
7048
}
49+
50+
@Param("true", "false")
51+
var withDebugger = false
52+
53+
@Setup
54+
fun setup() {
55+
DebugProbes.sanitizeStackTraces = false
56+
DebugProbes.enableCreationStackTraces = false
57+
if (withDebugger) {
58+
DebugProbes.install()
59+
}
60+
}
61+
62+
@TearDown
63+
fun tearDown() {
64+
if (withDebugger) {
65+
DebugProbes.uninstall()
66+
}
67+
}
68+
69+
// Shows the overhead of sequence builder with debugger enabled
70+
@Benchmark
71+
fun runSequenceSingleThread(): Int = runBlocking {
72+
generateRecursiveSequence((1..100).asSequence()) {
73+
(1..it).asSequence()
74+
}.sum()
75+
}
76+
77+
// Shows the overhead of sequence builder with debugger enabled and debugger is concurrently stressed out
78+
@Benchmark
79+
fun runSequenceMultipleThreads(): Int = runBlocking {
80+
val result = AtomicInteger(0)
81+
repeat(Runtime.getRuntime().availableProcessors()) {
82+
launch(Dispatchers.Default) {
83+
result.addAndGet(generateRecursiveSequence((1..100).asSequence()) {
84+
(1..it).asSequence()
85+
}.sum())
86+
}
87+
}
88+
result.get()
89+
}
7190
}

kotlinx-coroutines-core/api/kotlinx-coroutines-core.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,7 +938,9 @@ public final class kotlinx/coroutines/debug/internal/DebugProbesImpl {
938938
public final fun dumpDebuggerInfo ()Ljava/util/List;
939939
public final fun enhanceStackTraceWithThreadDump (Lkotlinx/coroutines/debug/internal/DebugCoroutineInfo;Ljava/util/List;)Ljava/util/List;
940940
public final fun enhanceStackTraceWithThreadDumpAsJson (Lkotlinx/coroutines/debug/internal/DebugCoroutineInfo;)Ljava/lang/String;
941+
public final fun getIgnoreCoroutinesWithEmptyContext ()Z
941942
public final fun isInstalled ()Z
943+
public final fun setIgnoreCoroutinesWithEmptyContext (Z)V
942944
}
943945

944946
public final class kotlinx/coroutines/debug/internal/DebugProbesImpl$CoroutineOwner : kotlin/coroutines/Continuation, kotlin/coroutines/jvm/internal/CoroutineStackFrame {

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ internal object DebugProbesImpl {
4242

4343
internal var sanitizeStackTraces: Boolean = true
4444
internal var enableCreationStackTraces: Boolean = true
45+
public var ignoreCoroutinesWithEmptyContext: Boolean = true
4546

4647
/*
4748
* Substitute for service loader, DI between core and debug modules.
@@ -422,8 +423,8 @@ internal object DebugProbesImpl {
422423

423424
private fun updateState(frame: Continuation<*>, state: String) {
424425
if (!isInstalled) return
425-
// KT-29997 is here only since 1.3.30
426-
if (state == RUNNING && KotlinVersion.CURRENT.isAtLeast(1, 3, 30)) {
426+
if (ignoreCoroutinesWithEmptyContext && frame.context === EmptyCoroutineContext) return // See ignoreCoroutinesWithEmptyContext
427+
if (state == RUNNING) {
427428
val stackFrame = frame as? CoroutineStackFrame ?: return
428429
updateRunningState(stackFrame, state)
429430
return
@@ -475,6 +476,8 @@ internal object DebugProbesImpl {
475476
// Not guarded by the lock at all, does not really affect consistency
476477
internal fun <T> probeCoroutineCreated(completion: Continuation<T>): Continuation<T> {
477478
if (!isInstalled) return completion
479+
// See DebugProbes.ignoreCoroutinesWithEmptyContext for the additional details.
480+
if (ignoreCoroutinesWithEmptyContext && completion.context === EmptyCoroutineContext) return completion
478481
/*
479482
* If completion already has an owner, it means that we are in scoped coroutine (coroutineScope, withContext etc.),
480483
* then piggyback on its already existing owner and do not replace completion

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public final class kotlinx/coroutines/debug/DebugProbes {
1818
public static synthetic fun dumpCoroutines$default (Lkotlinx/coroutines/debug/DebugProbes;Ljava/io/PrintStream;ILjava/lang/Object;)V
1919
public final fun dumpCoroutinesInfo ()Ljava/util/List;
2020
public final fun getEnableCreationStackTraces ()Z
21+
public final fun getIgnoreCoroutinesWithEmptyContext ()Z
2122
public final fun getSanitizeStackTraces ()Z
2223
public final fun install ()V
2324
public final fun isInstalled ()Z
@@ -28,6 +29,7 @@ public final class kotlinx/coroutines/debug/DebugProbes {
2829
public static synthetic fun printScope$default (Lkotlinx/coroutines/debug/DebugProbes;Lkotlinx/coroutines/CoroutineScope;Ljava/io/PrintStream;ILjava/lang/Object;)V
2930
public final fun scopeToString (Lkotlinx/coroutines/CoroutineScope;)Ljava/lang/String;
3031
public final fun setEnableCreationStackTraces (Z)V
32+
public final fun setIgnoreCoroutinesWithEmptyContext (Z)V
3133
public final fun setSanitizeStackTraces (Z)V
3234
public final fun uninstall ()V
3335
public final fun withDebugProbes (Lkotlin/jvm/functions/Function0;)V

kotlinx-coroutines-debug/src/DebugProbes.kt

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public object DebugProbes {
4646
* Whether coroutine creation stack traces should be sanitized.
4747
* Sanitization removes all frames from `kotlinx.coroutines` package except
4848
* the first one and the last one to simplify diagnostic.
49+
*
50+
* `true` by default.
4951
*/
5052
public var sanitizeStackTraces: Boolean
5153
get() = DebugProbesImpl.sanitizeStackTraces
@@ -59,13 +61,31 @@ public object DebugProbes {
5961
* thread is captured and attached to the coroutine.
6062
* This option can be useful during local debug sessions, but is recommended
6163
* to be disabled in production environments to avoid stack trace dumping overhead.
64+
*
65+
* `true` by default.
6266
*/
6367
public var enableCreationStackTraces: Boolean
6468
get() = DebugProbesImpl.enableCreationStackTraces
6569
set(value) {
6670
DebugProbesImpl.enableCreationStackTraces = value
6771
}
6872

73+
/**
74+
* Whether to ignore coroutines whose context is [EmptyCoroutineContext].
75+
*
76+
* Coroutines with empty context are considered to be irrelevant for the concurrent coroutines' observability:
77+
* - They do not contribute to any concurrent executions
78+
* - They do not contribute to the (concurrent) system's liveness and/or deadlocks, as no other coroutines might wait for them
79+
* - The typical usage of such coroutines is a combinator/builder/lookahead parser that can be debugged using more convenient tools.
80+
*
81+
* `true` by default.
82+
*/
83+
public var ignoreCoroutinesWithEmptyContext: Boolean
84+
get() = DebugProbesImpl.ignoreCoroutinesWithEmptyContext
85+
set(value) {
86+
DebugProbesImpl.ignoreCoroutinesWithEmptyContext = value
87+
}
88+
6989
/**
7090
* Determines whether debug probes were [installed][DebugProbes.install].
7191
*/
@@ -122,13 +142,14 @@ public object DebugProbes {
122142
* Throws [IllegalStateException] if the scope has no a job in it.
123143
*/
124144
public fun printScope(scope: CoroutineScope, out: PrintStream = System.out): Unit =
125-
printJob(scope.coroutineContext[Job] ?: error("Job is not present in the scope"), out)
145+
printJob(scope.coroutineContext[Job] ?: error("Job is not present in the scope"), out)
126146

127147
/**
128148
* Returns all existing coroutines' info.
129149
* The resulting collection represents a consistent snapshot of all existing coroutines at the moment of invocation.
130150
*/
131-
public fun dumpCoroutinesInfo(): List<CoroutineInfo> = DebugProbesImpl.dumpCoroutinesInfo().map { CoroutineInfo(it) }
151+
public fun dumpCoroutinesInfo(): List<CoroutineInfo> =
152+
DebugProbesImpl.dumpCoroutinesInfo().map { CoroutineInfo(it) }
132153

133154
/**
134155
* Dumps all active coroutines into the given output stream, providing a consistent snapshot of all existing coroutines at the moment of invocation.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2016-2023 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 org.junit.Test
7+
import kotlin.test.*
8+
9+
class StandardBuildersDebugTest : DebugTestBase() {
10+
11+
@Test
12+
fun testBuildersAreMissingFromDumpByDefault() = runTest {
13+
val (b1, b2) = createBuilders()
14+
15+
val coroutines = DebugProbes.dumpCoroutinesInfo()
16+
assertEquals(1, coroutines.size)
17+
assertTrue { b1.hasNext() && b2.hasNext() } // Don't let GC collect our coroutines until the test is complete
18+
}
19+
20+
@Test
21+
fun testBuildersCanBeEnabled() = runTest {
22+
try {
23+
DebugProbes.ignoreCoroutinesWithEmptyContext = false
24+
val (b1, b2) = createBuilders()
25+
val coroutines = DebugProbes.dumpCoroutinesInfo()
26+
assertEquals(3, coroutines.size)
27+
assertTrue { b1.hasNext() && b2.hasNext() } // Don't let GC collect our coroutines until the test is complete
28+
} finally {
29+
DebugProbes.ignoreCoroutinesWithEmptyContext = true
30+
}
31+
}
32+
33+
private fun createBuilders(): Pair<Iterator<Int>, Iterator<Int>> {
34+
val fromSequence = sequence {
35+
while (true) {
36+
yield(1)
37+
}
38+
}.iterator()
39+
40+
val fromIterator = iterator {
41+
while (true) {
42+
yield(1)
43+
}
44+
}
45+
// Start coroutines
46+
fromIterator.hasNext()
47+
fromSequence.hasNext()
48+
return fromSequence to fromIterator
49+
}
50+
}

0 commit comments

Comments
 (0)