Skip to content

Support static 'Debug Probes' #4246

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions integration-testing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The tests are the following:
* `debugAgentTest` checks that the coroutine debugger can be run as a Java agent.
* `debugDynamicAgentTest` checks that `kotlinx-coroutines-debug` agent can self-attach dynamically to JVM as a standalone dependency.
* `debugDynamicAgentJpmsTest` checks that `kotlinx-coroutines-debug` agent can self-attach dynamically to JVM as a standalone dependency (with JPMS)
* `externalStaticDebugProbesTest` checks that a `ExternalStaticDebugProbes` is picked up by the kotlin stdlib
* `smokeTest` builds the multiplatform test project that depends on coroutines.

The `integration-testing` project is expected to be in a subdirectory of the main `kotlinx.coroutines` project.
Expand Down
17 changes: 17 additions & 0 deletions integration-testing/jpmsTest/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ kotlin {
jvmToolchain(17)

val test = target.compilations.getByName("test")

target.compilations.create("debugDynamicAgentJpmsTest") {
associateWith(test)

Expand All @@ -35,6 +36,22 @@ kotlin {
classpath = javaSourceSet.runtimeClasspath
}
}


target.compilations.create("externalStaticDebugProbesTest") {
associateWith(test)


defaultSourceSet.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-debug:$coroutines_version")
}

tasks.register<Test>("externalStaticDebugProbesTest") {
testClassesDirs = output.classesDirs
classpath = javaSourceSet.runtimeClasspath
}
}
}

tasks.named("check") {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kotlinx.coroutines.external

import kotlinx.coroutines.debug.internal.AbstractStaticDebugProbes
import kotlin.coroutines.*

object ExternalStaticDebugProbes: AbstractStaticDebugProbes() {
override fun <T> probeCoroutineCreated(completion: Continuation<T>): Continuation<T> {
return super.probeCoroutineCreated(completion)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import org.junit.*
import kotlinx.coroutines.*
import kotlinx.coroutines.external.ExternalStaticDebugProbes
import org.junit.Test
import java.io.*

class ExternalStaticDebugProbesTest {

@Test
fun testDumpCoroutines() {
runBlocking {
val baos = ByteArrayOutputStream()
ExternalStaticDebugProbes.dumpCoroutines(PrintStream(baos))
// if the agent works, then dumps should contain something,
// at least the fact that this test is running.
val dump = baos.toString()
Assert.assertTrue(dump, dump.contains("testDumpCoroutines"))
}
}
}
8 changes: 8 additions & 0 deletions kotlinx-coroutines-core/api/kotlinx-coroutines-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,14 @@ public final class kotlinx/coroutines/channels/TickerMode : java/lang/Enum {
public static fun values ()[Lkotlinx/coroutines/channels/TickerMode;
}

public abstract class kotlinx/coroutines/debug/internal/AbstractStaticDebugProbes {
public fun <init> ()V
public final fun dumpCoroutines (Ljava/io/PrintStream;)V
public fun probeCoroutineCreated (Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation;
public fun probeCoroutineResumed (Lkotlin/coroutines/Continuation;)V
public fun probeCoroutineSuspended (Lkotlin/coroutines/Continuation;)V
}

public final class kotlinx/coroutines/debug/internal/AgentInstallationType {
public static final field INSTANCE Lkotlinx/coroutines/debug/internal/AgentInstallationType;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package kotlinx.coroutines.debug.internal

import kotlinx.coroutines.*
import java.io.*
import kotlin.coroutines.*

/**
* Allows to statically install 'Debug Probes' at the known location
* (kotlinx.coroutines.external.ExternalStaticDebugProbes).
*
* **Discussion**
*
* There are three methods of installing/engaging coroutines 'Debug Probes'
*
* 1) Dynamic Attach (using the 'kotlinx-coroutines-debug' module)
* This uses runtime byte-code alteration to replace the 'Debug Probes' straight from the kotlin-stdlib
*
* 2) Static Attach using an Agent
* This uses a java agent to replace the 'Debug Probes' from the kotlin-stdlib statically
*
* 3) ExternalStaticDebugProbes
* The kotlin-stdlib compiled against a class at
* `kotlinx.coroutines.external.ExternalStaticDebugProbes` which is not available at runtime, by default.
* If a class at this location is present, then the kotlin-stdlib will call into it.
*
* ```kotlin
* package kotlinx.coroutines.external
* object ExternalStaticDebugProbes: AbstractStaticDebugProbes() {
* override fun <T> probeCoroutineCreated(completion: Continuation<T>): Continuation<T> {
* // intercept
* // ...
*
* // forward to debugger machinery
* return super.probeCoroutineCreated(completion)
* }
* }
* ```
*/
@Suppress("unused")
@DelicateCoroutinesApi
@ExperimentalCoroutinesApi
abstract class AbstractStaticDebugProbes {
init {
require(javaClass.name == "kotlinx.coroutines.external.ExternalStaticDebugProbes")
AgentInstallationType.isInstalledStatically = true
DebugProbesImpl.install()
}

open fun probeCoroutineResumed(frame: Continuation<*>) = DebugProbesImpl.probeCoroutineResumed(frame)

open fun probeCoroutineSuspended(frame: Continuation<*>) = DebugProbesImpl.probeCoroutineSuspended(frame)

open fun <T> probeCoroutineCreated(completion: Continuation<T>): Continuation<T> =
DebugProbesImpl.probeCoroutineCreated(completion)

fun dumpCoroutines(out: PrintStream) {
DebugProbesImpl.dumpCoroutines(out)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ internal const val SUSPENDED = "SUSPENDED"
/**
* Internal implementation class where debugger tracks details it knows about each coroutine.
* Its mutable fields can be updated concurrently, thus marked with `@Volatile`
*
* Usage Note: IntelliJ/Coroutines Debugger: Reflection
*/
@PublishedApi
internal class DebugCoroutineInfoImpl internal constructor(
Expand Down
12 changes: 12 additions & 0 deletions kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ import kotlin.coroutines.jvm.internal.CoroutineStackFrame
import kotlin.synchronized
import _COROUTINE.ArtificialStackFrames

/**
* Usage Note: IntelliJ @SuppressWarnings({"KotlinInternalInJava"}): CoroutineDumpState
* call to 'install'
*
* Usage Note: IntelliJ @SuppressWarnings({"KotlinInternalInJava"}): DebugProbesKt
* Custom 'DebugProbesKt' class providing 'probeCoroutineCreated', 'probeCoroutineResumed', 'probeCoroutineSuspended'
* calling into DebugProbesImpl (similar to our DebugProbesKt)
*
* Usage Note: IntelliJ/Coroutines Debugger: Reflection
*/
@PublishedApi
internal object DebugProbesImpl {
private val ARTIFICIAL_FRAME = ArtificialStackFrames().coroutineCreation()
Expand Down Expand Up @@ -527,6 +537,8 @@ internal object DebugProbesImpl {
/**
* This class is injected as completion of all continuations in [probeCoroutineCompleted].
* It is owning the coroutine info and responsible for managing all its external info related to debug agent.
*
* Usage Note: IntelliJ/Coroutines Debugger: Reflection
*/
public class CoroutineOwner<T> internal constructor(
@JvmField internal val delegate: Continuation<T>,
Expand Down