Skip to content

Commit

Permalink
Support 'ExternalStaticDebugProbes' (in collaboration with the kotlin…
Browse files Browse the repository at this point in the history
…-stdlib)
  • Loading branch information
sellmair committed Oct 23, 2024
1 parent d30af7c commit c61d914
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 0 deletions.
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)
}
}

0 comments on commit c61d914

Please sign in to comment.