Skip to content

Commit

Permalink
Do not track coroutines with empty coroutine context in DebugProbes
Browse files Browse the repository at this point in the history
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
  • Loading branch information
qwwdfsad committed Jun 28, 2023
1 parent 1e1240c commit 0b56ecd
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ internal object DebugProbesImpl {

internal var sanitizeStackTraces: Boolean = true
internal var enableCreationStackTraces: Boolean = true
public var ignoreCoroutinesWithEmptyContext = true

/*
* Substitute for service loader, DI between core and debug modules.
Expand Down Expand Up @@ -422,6 +423,7 @@ internal object DebugProbesImpl {

private fun updateState(frame: Continuation<*>, state: String) {
if (!isInstalled) return
if (ignoreCoroutinesWithEmptyContext && frame.context === EmptyCoroutineContext) return // See ignoreCoroutinesWithEmptyContext
// KT-29997 is here only since 1.3.30
if (state == RUNNING && KotlinVersion.CURRENT.isAtLeast(1, 3, 30)) {
val stackFrame = frame as? CoroutineStackFrame ?: return
Expand Down Expand Up @@ -475,6 +477,8 @@ internal object DebugProbesImpl {
// Not guarded by the lock at all, does not really affect consistency
internal fun <T> probeCoroutineCreated(completion: Continuation<T>): Continuation<T> {
if (!isInstalled) return completion
// See DebugProbes.ignoreCoroutinesWithEmptyContext for the additional details.
if (ignoreCoroutinesWithEmptyContext && completion.context == EmptyCoroutineContext) return completion
/*
* If completion already has an owner, it means that we are in scoped coroutine (coroutineScope, withContext etc.),
* then piggyback on its already existing owner and do not replace completion
Expand Down
25 changes: 23 additions & 2 deletions kotlinx-coroutines-debug/src/DebugProbes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public object DebugProbes {
* Whether coroutine creation stack traces should be sanitized.
* Sanitization removes all frames from `kotlinx.coroutines` package except
* the first one and the last one to simplify diagnostic.
*
* `true` by default.
*/
public var sanitizeStackTraces: Boolean
get() = DebugProbesImpl.sanitizeStackTraces
Expand All @@ -59,13 +61,31 @@ public object DebugProbes {
* thread is captured and attached to the coroutine.
* This option can be useful during local debug sessions, but is recommended
* to be disabled in production environments to avoid stack trace dumping overhead.
*
* `true` by default.
*/
public var enableCreationStackTraces: Boolean
get() = DebugProbesImpl.enableCreationStackTraces
set(value) {
DebugProbesImpl.enableCreationStackTraces = value
}

/**
* Whether to ignore coroutines that have their context equal to [EmptyCoroutineContext].
*
* Coroutines with empty context are considered to be irrelevant for the concurrent's coroutines observability:
* - They do not contribute to any concurrent executions
* - They do not contribute to the (concurrent) system's liveness and/or deadlocks as no other coroutines might wait for them
* - The typical usage of such coroutines is a combinator/builder/lookahead parser that can be debugged using more convenient tools.
*
* `true` by default.
*/
public var ignoreCoroutinesWithEmptyContext: Boolean
get() = DebugProbesImpl.ignoreCoroutinesWithEmptyContext
set(value) {
DebugProbesImpl.ignoreCoroutinesWithEmptyContext = value
}

/**
* Determines whether debug probes were [installed][DebugProbes.install].
*/
Expand Down Expand Up @@ -122,13 +142,14 @@ public object DebugProbes {
* Throws [IllegalStateException] if the scope has no a job in it.
*/
public fun printScope(scope: CoroutineScope, out: PrintStream = System.out): Unit =
printJob(scope.coroutineContext[Job] ?: error("Job is not present in the scope"), out)
printJob(scope.coroutineContext[Job] ?: error("Job is not present in the scope"), out)

/**
* Returns all existing coroutines' info.
* The resulting collection represents a consistent snapshot of all existing coroutines at the moment of invocation.
*/
public fun dumpCoroutinesInfo(): List<CoroutineInfo> = DebugProbesImpl.dumpCoroutinesInfo().map { CoroutineInfo(it) }
public fun dumpCoroutinesInfo(): List<CoroutineInfo> =
DebugProbesImpl.dumpCoroutinesInfo().map { CoroutineInfo(it) }

/**
* Dumps all active coroutines into the given output stream, providing a consistent snapshot of all existing coroutines at the moment of invocation.
Expand Down
31 changes: 31 additions & 0 deletions kotlinx-coroutines-debug/test/StandardBuildersAreIgnoredTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
package kotlinx.coroutines.debug

import org.junit.Test
import kotlin.test.*

class StandardBuildersAreIgnoredTest : DebugTestBase() {

@Test
fun testBuildersAreMissingFromDump() = runTest {
val fromSequence = sequence {
while (true) {
yield(1)
}
}.iterator()

val fromIterator = iterator {
while (true) {
yield(1)
}
}
// Start coroutines
fromIterator.hasNext()
fromSequence.hasNext()

val coroutines = DebugProbes.dumpCoroutinesInfo()
assertEquals(1, coroutines.size)
}
}

0 comments on commit 0b56ecd

Please sign in to comment.