From f96c305780e871c323a7e5a5515891521eb1a890 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 6 Apr 2020 18:02:18 +0300 Subject: [PATCH 1/7] Debug agent machinery rework in order to prepare for IDEA integration * Extract DebugProbesImpl, CoroutineInfo and State to kotlinx-coroutines-core * Introduce AgentPremain that works without ByteBuddy to kotlinx-coroutines-core, so it now can be used as Java agent and all debug info can be extracted via reflection or JDWP * Reflective lookup of ByteBuddy attach to resolve cyclic dependency between core and debug modules --- integration-testing/build.gradle | 17 ++++- .../src/coreAgentTest/kotlin/CoreAgentTest.kt | 22 ++++++ .../debugAgentTest/kotlin/DebugAgentTest.kt | 5 +- .../api/kotlinx-coroutines-core.api | 43 ++++++++++++ kotlinx-coroutines-core/build.gradle | 7 ++ .../jvm/resources/DebugProbesKt.bin | Bin 0 -> 1628 bytes .../jvm/src/debug/AgentPremain.kt | 65 ++++++++++++++++++ .../jvm/src/debug}/CoroutineInfo.kt | 13 +++- .../src/debug}/internal/DebugProbesImpl.kt | 34 +++------ .../jvm/src/debug/internal/DynamicAttach.kt | 18 +++++ .../api/kotlinx-coroutines-debug.api | 22 ------ kotlinx-coroutines-debug/src/AgentPremain.kt | 41 ----------- kotlinx-coroutines-debug/src/DebugProbes.kt | 16 +++-- .../src/internal/Attach.kt | 39 +++++++++++ .../testdata/r8-test-rules.pro | 3 +- 15 files changed, 246 insertions(+), 99 deletions(-) create mode 100644 integration-testing/src/coreAgentTest/kotlin/CoreAgentTest.kt create mode 100644 kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin create mode 100644 kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt rename {kotlinx-coroutines-debug/src => kotlinx-coroutines-core/jvm/src/debug}/CoroutineInfo.kt (91%) rename {kotlinx-coroutines-debug/src => kotlinx-coroutines-core/jvm/src/debug}/internal/DebugProbesImpl.kt (94%) create mode 100644 kotlinx-coroutines-core/jvm/src/debug/internal/DynamicAttach.kt delete mode 100644 kotlinx-coroutines-debug/src/AgentPremain.kt create mode 100644 kotlinx-coroutines-debug/src/internal/Attach.kt diff --git a/integration-testing/build.gradle b/integration-testing/build.gradle index f376f667e0..a7e3333fd5 100644 --- a/integration-testing/build.gradle +++ b/integration-testing/build.gradle @@ -25,6 +25,12 @@ sourceSets { compileClasspath += sourceSets.test.runtimeClasspath runtimeClasspath += sourceSets.test.runtimeClasspath } + + coreAgentTest { + kotlin + compileClasspath += sourceSets.test.runtimeClasspath + runtimeClasspath += sourceSets.test.runtimeClasspath + } } task npmTest(type: Test) { @@ -62,6 +68,14 @@ task debugAgentTest(type: Test) { classpath = sourceSet.runtimeClasspath } +task coreAgentTest(type: Test) { + def sourceSet = sourceSets.coreAgentTest + dependsOn(project(':kotlinx-coroutines-core').jvmJar) + jvmArgs ('-javaagent:' + project(':kotlinx-coroutines-core').jvmJar.outputs.files.getFiles()[0]) + testClassesDirs = sourceSet.output.classesDirs + classpath = sourceSet.runtimeClasspath +} + dependencies { testCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" testCompile 'junit:junit:4.12' @@ -69,6 +83,7 @@ dependencies { npmTestCompile 'com.google.code.gson:gson:2.8.5' debugAgentTestCompile project(':kotlinx-coroutines-core') debugAgentTestCompile project(':kotlinx-coroutines-debug') + coreAgentTestCompile project(':kotlinx-coroutines-core') } compileTestKotlin { @@ -76,5 +91,5 @@ compileTestKotlin { } test { - dependsOn([npmTest, mavenTest, debugAgentTest]) + dependsOn([npmTest, mavenTest, debugAgentTest, coreAgentTest]) } diff --git a/integration-testing/src/coreAgentTest/kotlin/CoreAgentTest.kt b/integration-testing/src/coreAgentTest/kotlin/CoreAgentTest.kt new file mode 100644 index 0000000000..6d47323370 --- /dev/null +++ b/integration-testing/src/coreAgentTest/kotlin/CoreAgentTest.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +import org.junit.* +import kotlinx.coroutines.* +import kotlinx.coroutines.debug.internal.* +import org.junit.Test +import java.io.* + +class CoreAgentTest { + + @Test + fun testAgentDumpsCoroutines() = runBlocking { + val baos = ByteArrayOutputStream() + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + DebugProbesImpl.dumpCoroutines(PrintStream(baos)) + // if the agent works, then dumps should contain something, + // at least the fact that this test is running. + Assert.assertTrue(baos.toString().contains("testAgentDumpsCoroutines")) + } + +} diff --git a/integration-testing/src/debugAgentTest/kotlin/DebugAgentTest.kt b/integration-testing/src/debugAgentTest/kotlin/DebugAgentTest.kt index 925fe077ea..d6c4aa2fb4 100644 --- a/integration-testing/src/debugAgentTest/kotlin/DebugAgentTest.kt +++ b/integration-testing/src/debugAgentTest/kotlin/DebugAgentTest.kt @@ -4,17 +4,18 @@ import org.junit.* import kotlinx.coroutines.* import kotlinx.coroutines.debug.* +import org.junit.Test import java.io.* class DebugAgentTest { @Test - fun agentDumpsCoroutines() = runBlocking { + fun testAgentDumpsCoroutines() = runBlocking { val baos = ByteArrayOutputStream() DebugProbes.dumpCoroutines(PrintStream(baos)) // if the agent works, then dumps should contain something, // at least the fact that this test is running. - Assert.assertTrue(baos.toString().contains("agentDumpsCoroutines")) + Assert.assertTrue(baos.toString().contains("testAgentDumpsCoroutines")) } } diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 54e355ec37..221a93f3fd 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -832,6 +832,49 @@ public final class kotlinx/coroutines/channels/ValueOrClosed { public final synthetic fun unbox-impl ()Ljava/lang/Object; } +public final class kotlinx/coroutines/debug/CoroutineInfo { + public final fun copy ()Lkotlinx/coroutines/debug/CoroutineInfo; + public final fun getContext ()Lkotlin/coroutines/CoroutineContext; + public final fun getCreationStackTrace ()Ljava/util/List; + public final fun getJob ()Lkotlinx/coroutines/Job; + public final fun getState ()Lkotlinx/coroutines/debug/State; + public final fun lastObservedStackTrace ()Ljava/util/List; + public fun toString ()Ljava/lang/String; +} + +public final class kotlinx/coroutines/debug/DebugProbes { + public static final field INSTANCE Lkotlinx/coroutines/debug/DebugProbes; + public final fun dumpCoroutines (Ljava/io/PrintStream;)V + public static synthetic fun dumpCoroutines$default (Lkotlinx/coroutines/debug/DebugProbes;Ljava/io/PrintStream;ILjava/lang/Object;)V + public final fun dumpCoroutinesInfo ()Ljava/util/List; + public final fun getEnableCreationStackTraces ()Z + public final fun getSanitizeStackTraces ()Z + public final fun install ()V + public final fun isInstalled ()Z + public final fun jobToString (Lkotlinx/coroutines/Job;)Ljava/lang/String; + public final fun printJob (Lkotlinx/coroutines/Job;Ljava/io/PrintStream;)V + public static synthetic fun printJob$default (Lkotlinx/coroutines/debug/DebugProbes;Lkotlinx/coroutines/Job;Ljava/io/PrintStream;ILjava/lang/Object;)V + public final fun printScope (Lkotlinx/coroutines/CoroutineScope;Ljava/io/PrintStream;)V + public static synthetic fun printScope$default (Lkotlinx/coroutines/debug/DebugProbes;Lkotlinx/coroutines/CoroutineScope;Ljava/io/PrintStream;ILjava/lang/Object;)V + public final fun scopeToString (Lkotlinx/coroutines/CoroutineScope;)Ljava/lang/String; + public final fun setEnableCreationStackTraces (Z)V + public final fun setSanitizeStackTraces (Z)V + public final fun uninstall ()V + public final fun withDebugProbes (Lkotlin/jvm/functions/Function0;)V +} + +public final class kotlinx/coroutines/debug/State : java/lang/Enum { + public static final field CREATED Lkotlinx/coroutines/debug/State; + public static final field RUNNING Lkotlinx/coroutines/debug/State; + public static final field SUSPENDED Lkotlinx/coroutines/debug/State; + public static fun valueOf (Ljava/lang/String;)Lkotlinx/coroutines/debug/State; + public static fun values ()[Lkotlinx/coroutines/debug/State; +} + +public synthetic class kotlinx/coroutines/debug/internal/DebugProbesImplSequenceNumberRefVolatile { + public fun (J)V +} + public abstract class kotlinx/coroutines/flow/AbstractFlow : kotlinx/coroutines/flow/Flow { public fun ()V public final fun collect (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index 547a12b4c6..5f175f865c 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -92,6 +92,13 @@ jvmTest { systemProperty 'kotlinx.coroutines.scheduler.keep.alive.sec', '100000' // any unpark problem hangs test } +jvmJar { + manifest { + attributes "Premain-Class": "kotlinx.coroutines.debug.AgentPremain" + attributes "Can-Retransform-Classes": "true" + } +} + task jvmStressTest(type: Test, dependsOn: compileTestKotlinJvm) { classpath = files { jvmTest.classpath } testClassesDirs = files { jvmTest.testClassesDirs } diff --git a/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin b/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin new file mode 100644 index 0000000000000000000000000000000000000000..0787b8ce128b9fefb4f23becc8470a4c3780d58b GIT binary patch literal 1628 zcmb7ET~8B16g{&oZCNWUmCquGfCvaIEq+o$f>lF9Erd3_@L;CxNEX^%va{Rx$j|Xn zeel5tLwxi{8SgA@3I$S4+MT&~?!D*Ub9VOkpC3N~q_NJhbmDk5%Qh>H>-e5!3uzuV z>ZWCT!nJwLd?U*K(Vpv+h1~I?U<}4FZ*a55?IUxyd@L&D4KWOzs+er6O4b$J6IF)S z^ZDxtS;r>LXBN5zFjO{5>-kRD%x1UJ8_T6qX0t26Mi9j?*lMI`*%h%9@RDJO zHiRpwQHD@zWsPAb@3=?ivGB?+w`^&0+jhKQ6loS5ui)2el$E-e`qO1%tBT7Tsm;|- zYf;1*V!1-Gv{lHyWms#r^B*NLarB|T7k6PWEZ?ZeY_qQqZNb%6#F0Qmr_6f{dc~oB zgi?oLs>Mn>!Mvj1HSPC>^y^CdnQj7Q77iHZxILWWo8zH)4Og%^otsIz8CS4YG zqBa`&t0@Ni{4AlLpXu7Drfac~t{K6aoHowSlCeZMp>1ocY&sk}KQpv++z7Ru+blU~ zgp=B8Pg*x3$w-3oSJ^h@4I0rK+`m3YVhS})J(L*rhmq#{u-ukcZHcVDOILs8#A66& zohteJ^YoP{`1P`IOT1hoPa^MBc18nu8XThWJ|MF;-1sD_t8KPkMTb(i{|CYELn5z1jU_&vg0%X*x{ jJQi|T#FIlXi6tx_Dh7Hah9y=ch9pKLOo<7JNr~0JKP{iI literal 0 HcmV?d00001 diff --git a/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt new file mode 100644 index 0000000000..6ce9bbb3fe --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug + +import kotlinx.coroutines.debug.internal.DebugProbesImpl +import sun.misc.* +import java.lang.instrument.* +import java.lang.instrument.ClassFileTransformer +import java.security.* + +@Suppress("unused") +internal object AgentPremain { + + private val enableCreationStackTraces = + System.getProperty("kotlinx.coroutines.debug.enable.creation.stack.trace")?.toBoolean() + ?: DebugProbesImpl.enableCreationStackTraces + + @JvmStatic + public fun premain(args: String?, instrumentation: Instrumentation) { + instrumentation.addTransformer(DebugProbesTransformer) + DebugProbesImpl.enableCreationStackTraces = enableCreationStackTraces + DebugProbesImpl.install() + installSignalHandler() + } + + internal object DebugProbesTransformer : ClassFileTransformer { + override fun transform( + loader: ClassLoader, + className: String, + classBeingRedefined: Class<*>?, + protectionDomain: ProtectionDomain, + classfileBuffer: ByteArray? + ): ByteArray? { + val name = className.replace("/", ".") + if (name != "kotlin.coroutines.jvm.internal.DebugProbesKt") { + return null + } + /* + * DebugProbesKt.bin contains `kotlin.coroutines.jvm.internal.DebugProbesKt` class + * with method bodies that delegate all calls directly to their counterparts in + * kotlinx.coroutines.debug.DebugProbesImpl. This is done to avoid classfile patching + * on the fly (-> get rid of ASM dependency) + */ + return loader.getResourceAsStream("DebugProbesKt.bin").readBytes() + } + } + + private fun installSignalHandler() { + try { + Signal.handle(Signal("TRAP")) { // kill -5 + if (DebugProbesImpl.isInstalled) { + // Case with 'isInstalled' changed between this check-and-act is not considered + // a real debug probes use-case, thus is not guarded against. + DebugProbesImpl.dumpCoroutines(System.out) + } else { + println("""Cannot perform coroutines dump, debug probes are disabled""") + } + } + } catch (t: Throwable) { + System.err.println("Failed to install signal handler: $t") + } + } +} diff --git a/kotlinx-coroutines-debug/src/CoroutineInfo.kt b/kotlinx-coroutines-core/jvm/src/debug/CoroutineInfo.kt similarity index 91% rename from kotlinx-coroutines-debug/src/CoroutineInfo.kt rename to kotlinx-coroutines-core/jvm/src/debug/CoroutineInfo.kt index c9517fe897..a3dc74fe48 100644 --- a/kotlinx-coroutines-debug/src/CoroutineInfo.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/CoroutineInfo.kt @@ -12,9 +12,13 @@ import kotlin.coroutines.jvm.internal.* /** * Class describing coroutine info such as its context, state and stacktrace. + * This API is usable only along with `kotlinx-coroutines-debug` module. */ @ExperimentalCoroutinesApi public class CoroutineInfo internal constructor( + /** + * Coroutine context of the coroutine + */ public val context: CoroutineContext, private val creationStackBottom: CoroutineStackFrame?, @JvmField internal val sequenceNumber: Long @@ -37,7 +41,8 @@ public class CoroutineInfo internal constructor( */ public val state: State get() = _state - private var _state: State = State.CREATED + private var _state: State = + State.CREATED @JvmField internal var lastObservedThread: Thread? = null @@ -45,7 +50,11 @@ public class CoroutineInfo internal constructor( @JvmField internal var lastObservedFrame: CoroutineStackFrame? = null - public fun copy(): CoroutineInfo = CoroutineInfo(context, creationStackBottom, sequenceNumber).also { + public fun copy(): CoroutineInfo = CoroutineInfo( + context, + creationStackBottom, + sequenceNumber + ).also { it._state = _state it.lastObservedFrame = lastObservedFrame it.lastObservedThread = lastObservedThread diff --git a/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt similarity index 94% rename from kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt rename to kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt index 8b7d8e7998..2b990f4088 100644 --- a/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt @@ -7,9 +7,6 @@ package kotlinx.coroutines.debug.internal import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlinx.coroutines.debug.* -import net.bytebuddy.* -import net.bytebuddy.agent.* -import net.bytebuddy.dynamic.loading.* import java.io.* import java.text.* import java.util.* @@ -44,6 +41,9 @@ internal object DebugProbesImpl { */ private val coroutineStateLock = ReentrantReadWriteLock() + public var sanitizeStackTraces: Boolean = true + public var enableCreationStackTraces: Boolean = true + /* * This is an optimization in the face of KT-29997: * Consider suspending call stack a()->b()->c() and c() completes its execution and every call is @@ -56,32 +56,15 @@ internal object DebugProbesImpl { public fun install(): Unit = coroutineStateLock.write { if (++installations > 1) return - - ByteBuddyAgent.install(ByteBuddyAgent.AttachmentProvider.ForEmulatedAttachment.INSTANCE) - val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") - val cl2 = Class.forName("kotlinx.coroutines.debug.DebugProbesKt") - - ByteBuddy() - .redefine(cl2) - .name(cl.name) - .make() - .load(cl.classLoader, ClassReloadingStrategy.fromInstalledAgent()) + attach.invoke() } public fun uninstall(): Unit = coroutineStateLock.write { check(isInstalled) { "Agent was not installed" } if (--installations != 0) return - capturedCoroutines.clear() callerInfoCache.clear() - val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") - val cl2 = Class.forName("kotlinx.coroutines.debug.internal.NoOpProbesKt") - - ByteBuddy() - .redefine(cl2) - .name(cl.name) - .make() - .load(cl.classLoader, ClassReloadingStrategy.fromInstalledAgent()) + detach.invoke() } public fun hierarchyToString(job: Job): String = coroutineStateLock.write { @@ -213,7 +196,8 @@ internal object DebugProbesImpl { val (continuationStartFrame, frameSkipped) = findContinuationStartIndex( indexOfResumeWith, actualTrace, - coroutineTrace) + coroutineTrace + ) if (continuationStartFrame == -1) return coroutineTrace @@ -335,7 +319,7 @@ internal object DebugProbesImpl { * and then using CoroutineOwner completion as unique identifier of coroutineSuspended/resumed calls. */ - val frame = if (DebugProbes.enableCreationStackTraces) { + val frame = if (enableCreationStackTraces) { val stacktrace = sanitizeStackTrace(Exception()) stacktrace.foldRight(null) { frame, acc -> object : CoroutineStackFrame { @@ -398,7 +382,7 @@ internal object DebugProbesImpl { val size = stackTrace.size val probeIndex = stackTrace.indexOfLast { it.className == "kotlin.coroutines.jvm.internal.DebugProbesKt" } - if (!DebugProbes.sanitizeStackTraces) { + if (!sanitizeStackTraces) { return List(size - probeIndex) { if (it == 0) createArtificialFrame(ARTIFICIAL_FRAME_MESSAGE) else stackTrace[it + probeIndex] } diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DynamicAttach.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DynamicAttach.kt new file mode 100644 index 0000000000..498613102f --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DynamicAttach.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.coroutines.debug.internal + +private val noop = {} +/* + * Use dynamic attach if kotlinx-coroutines-debug is present in the classpath + */ +internal val attach = getFunction("kotlinx.coroutines.debug.internal.ByteBuddyDynamicAttach") ?: noop +internal val detach = getFunction("kotlinx.coroutines.debug.internal.ByteBuddyDynamicDetach") ?: noop + +@Suppress("UNCHECKED_CAST") +private fun getFunction(clzName: String): Function0? = runCatching { + val clz = Class.forName(clzName) + val ctor = clz.constructors[0] + ctor.newInstance() as Function0 +}.getOrNull() diff --git a/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api b/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api index 749c94619e..3020519b84 100644 --- a/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api +++ b/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api @@ -1,13 +1,3 @@ -public final class kotlinx/coroutines/debug/CoroutineInfo { - public final fun copy ()Lkotlinx/coroutines/debug/CoroutineInfo; - public final fun getContext ()Lkotlin/coroutines/CoroutineContext; - public final fun getCreationStackTrace ()Ljava/util/List; - public final fun getJob ()Lkotlinx/coroutines/Job; - public final fun getState ()Lkotlinx/coroutines/debug/State; - public final fun lastObservedStackTrace ()Ljava/util/List; - public fun toString ()Ljava/lang/String; -} - public final class kotlinx/coroutines/debug/CoroutinesBlockHoundIntegration : reactor/blockhound/integration/BlockHoundIntegration { public fun ()V public fun applyTo (Lreactor/blockhound/BlockHound$Builder;)V @@ -34,18 +24,6 @@ public final class kotlinx/coroutines/debug/DebugProbes { public final fun withDebugProbes (Lkotlin/jvm/functions/Function0;)V } -public final class kotlinx/coroutines/debug/State : java/lang/Enum { - public static final field CREATED Lkotlinx/coroutines/debug/State; - public static final field RUNNING Lkotlinx/coroutines/debug/State; - public static final field SUSPENDED Lkotlinx/coroutines/debug/State; - public static fun valueOf (Ljava/lang/String;)Lkotlinx/coroutines/debug/State; - public static fun values ()[Lkotlinx/coroutines/debug/State; -} - -public synthetic class kotlinx/coroutines/debug/internal/DebugProbesImplSequenceNumberRefVolatile { - public fun (J)V -} - public final class kotlinx/coroutines/debug/junit4/CoroutinesTimeout : org/junit/rules/TestRule { public static final field Companion Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout$Companion; public fun (JZ)V diff --git a/kotlinx-coroutines-debug/src/AgentPremain.kt b/kotlinx-coroutines-debug/src/AgentPremain.kt deleted file mode 100644 index aa842ce327..0000000000 --- a/kotlinx-coroutines-debug/src/AgentPremain.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.debug - -import net.bytebuddy.agent.* -import sun.misc.* -import java.lang.instrument.* - -@Suppress("unused") -internal object AgentPremain { - - private val enableCreationStackTraces = - System.getProperty("kotlinx.coroutines.debug.enable.creation.stack.trace")?.toBoolean() - ?: DebugProbes.enableCreationStackTraces - - @JvmStatic - public fun premain(args: String?, instrumentation: Instrumentation) { - Installer.premain(args, instrumentation) - DebugProbes.enableCreationStackTraces = enableCreationStackTraces - DebugProbes.install() - installSignalHandler() - } - - private fun installSignalHandler() { - try { - Signal.handle(Signal("TRAP")) { // kill -5 - if (DebugProbes.isInstalled) { - // Case with 'isInstalled' changed between this check-and-act is not considered - // a real debug probes use-case, thus is not guarded against. - DebugProbes.dumpCoroutines() - } else { - println("""Cannot perform coroutines dump, debug probes are disabled""") - } - } - } catch (t: Throwable) { - System.err.println("Failed to install signal handler: $t") - } - } -} diff --git a/kotlinx-coroutines-debug/src/DebugProbes.kt b/kotlinx-coroutines-debug/src/DebugProbes.kt index 710300cb37..4a828ecab9 100644 --- a/kotlinx-coroutines-debug/src/DebugProbes.kt +++ b/kotlinx-coroutines-debug/src/DebugProbes.kt @@ -2,7 +2,7 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -@file:Suppress("unused") +@file:Suppress("UNUSED", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") package kotlinx.coroutines.debug @@ -10,7 +10,6 @@ import kotlinx.coroutines.* import kotlinx.coroutines.debug.internal.* import java.io.* import java.lang.management.* -import java.util.* import kotlin.coroutines.* /** @@ -41,7 +40,11 @@ public object DebugProbes { * Sanitization removes all frames from `kotlinx.coroutines` package except * the first one and the last one to simplify diagnostic. */ - public var sanitizeStackTraces: Boolean = true + public var sanitizeStackTraces: Boolean + get() = DebugProbesImpl.sanitizeStackTraces + set(value) { + DebugProbesImpl.sanitizeStackTraces = value + } /** * Whether coroutine creation stack traces should be captured. @@ -50,7 +53,11 @@ public object DebugProbes { * This option can be useful during local debug sessions, but is recommended * to be disabled in production environments to avoid stack trace dumping overhead. */ - public var enableCreationStackTraces: Boolean = true + public var enableCreationStackTraces: Boolean + get() = DebugProbesImpl.enableCreationStackTraces + set(value) { + DebugProbesImpl.enableCreationStackTraces = value + } /** * Determines whether debug probes were [installed][DebugProbes.install]. @@ -131,7 +138,6 @@ public object DebugProbes { * at MyClass.createIoRequest(MyClass.kt:142) * at MyClass.fetchData(MyClass.kt:154) * at MyClass.showData(MyClass.kt:31) - * * ... * ``` */ diff --git a/kotlinx-coroutines-debug/src/internal/Attach.kt b/kotlinx-coroutines-debug/src/internal/Attach.kt new file mode 100644 index 0000000000..eedf7ad96f --- /dev/null +++ b/kotlinx-coroutines-debug/src/internal/Attach.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:Suppress("unused") +package kotlinx.coroutines.debug.internal + +import net.bytebuddy.* +import net.bytebuddy.agent.* +import net.bytebuddy.dynamic.loading.* + +/* + * These classes are used reflectively from kotlinx-coroutines-core when this module is present in the classpath. + * It is a substitute for service loading + */ +internal class ByteBuddyDynamicAttach : Function0 { + override fun invoke() { + ByteBuddyAgent.install(ByteBuddyAgent.AttachmentProvider.ForEmulatedAttachment.INSTANCE) + val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") + val cl2 = Class.forName("kotlinx.coroutines.debug.DebugProbesKt") + + ByteBuddy() + .redefine(cl2) + .name(cl.name) + .make() + .load(cl.classLoader, ClassReloadingStrategy.fromInstalledAgent()) + } +} + +internal class ByteBuddyDynamicDetach : Function0 { + override fun invoke() { + val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") + val cl2 = Class.forName("kotlinx.coroutines.debug.internal.NoOpProbesKt") + ByteBuddy() + .redefine(cl2) + .name(cl.name) + .make() + .load(cl.classLoader, ClassReloadingStrategy.fromInstalledAgent()) + } +} diff --git a/ui/kotlinx-coroutines-android/testdata/r8-test-rules.pro b/ui/kotlinx-coroutines-android/testdata/r8-test-rules.pro index dde8600854..63dc24ccd5 100644 --- a/ui/kotlinx-coroutines-android/testdata/r8-test-rules.pro +++ b/ui/kotlinx-coroutines-android/testdata/r8-test-rules.pro @@ -6,9 +6,10 @@ -checkdiscard class kotlinx.coroutines.internal.FastServiceLoader -checkdiscard class kotlinx.coroutines.DebugKt -checkdiscard class kotlinx.coroutines.internal.StackTraceRecoveryKt +-checkdiscard class kotlinx.coroutines.debug.DebugProbesKt # Real android projects do not keep this class, but somehow it is kept in this test (R8 bug) # -checkdiscard class kotlinx.coroutines.internal.MissingMainCoroutineDispatcher # Should not keep this class, but it is still there (R8 bug) -#-checkdiscard class kotlinx.coroutines.CoroutineId \ No newline at end of file +#-checkdiscard class kotlinx.coroutines.CoroutineId From 6dd75ebc1ee22ab4f155e03660bda7fd386ed9f2 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 6 Apr 2020 18:36:03 +0300 Subject: [PATCH 2/7] Reduce public API surface, introduce JDWP-specific API --- .../api/kotlinx-coroutines-core.api | 39 --------- .../src/debug/internal/DebugCoroutineInfo.kt | 87 +++++++++++++++++++ .../jvm/src/debug/internal/DebugProbesImpl.kt | 42 ++++----- .../jvm/src/debug/internal/DebuggerInfo.kt | 20 +++++ .../jvm/src/debug/internal/DynamicAttach.kt | 1 + .../api/kotlinx-coroutines-debug.api | 17 ++++ .../src}/CoroutineInfo.kt | 57 +++--------- kotlinx-coroutines-debug/src/DebugProbes.kt | 2 +- .../test/RunningThreadStackMergeTest.kt | 6 +- 9 files changed, 160 insertions(+), 111 deletions(-) create mode 100644 kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfo.kt create mode 100644 kotlinx-coroutines-core/jvm/src/debug/internal/DebuggerInfo.kt rename {kotlinx-coroutines-core/jvm/src/debug => kotlinx-coroutines-debug/src}/CoroutineInfo.kt (62%) diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 221a93f3fd..08f46e5108 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -832,45 +832,6 @@ public final class kotlinx/coroutines/channels/ValueOrClosed { public final synthetic fun unbox-impl ()Ljava/lang/Object; } -public final class kotlinx/coroutines/debug/CoroutineInfo { - public final fun copy ()Lkotlinx/coroutines/debug/CoroutineInfo; - public final fun getContext ()Lkotlin/coroutines/CoroutineContext; - public final fun getCreationStackTrace ()Ljava/util/List; - public final fun getJob ()Lkotlinx/coroutines/Job; - public final fun getState ()Lkotlinx/coroutines/debug/State; - public final fun lastObservedStackTrace ()Ljava/util/List; - public fun toString ()Ljava/lang/String; -} - -public final class kotlinx/coroutines/debug/DebugProbes { - public static final field INSTANCE Lkotlinx/coroutines/debug/DebugProbes; - public final fun dumpCoroutines (Ljava/io/PrintStream;)V - public static synthetic fun dumpCoroutines$default (Lkotlinx/coroutines/debug/DebugProbes;Ljava/io/PrintStream;ILjava/lang/Object;)V - public final fun dumpCoroutinesInfo ()Ljava/util/List; - public final fun getEnableCreationStackTraces ()Z - public final fun getSanitizeStackTraces ()Z - public final fun install ()V - public final fun isInstalled ()Z - public final fun jobToString (Lkotlinx/coroutines/Job;)Ljava/lang/String; - public final fun printJob (Lkotlinx/coroutines/Job;Ljava/io/PrintStream;)V - public static synthetic fun printJob$default (Lkotlinx/coroutines/debug/DebugProbes;Lkotlinx/coroutines/Job;Ljava/io/PrintStream;ILjava/lang/Object;)V - public final fun printScope (Lkotlinx/coroutines/CoroutineScope;Ljava/io/PrintStream;)V - public static synthetic fun printScope$default (Lkotlinx/coroutines/debug/DebugProbes;Lkotlinx/coroutines/CoroutineScope;Ljava/io/PrintStream;ILjava/lang/Object;)V - public final fun scopeToString (Lkotlinx/coroutines/CoroutineScope;)Ljava/lang/String; - public final fun setEnableCreationStackTraces (Z)V - public final fun setSanitizeStackTraces (Z)V - public final fun uninstall ()V - public final fun withDebugProbes (Lkotlin/jvm/functions/Function0;)V -} - -public final class kotlinx/coroutines/debug/State : java/lang/Enum { - public static final field CREATED Lkotlinx/coroutines/debug/State; - public static final field RUNNING Lkotlinx/coroutines/debug/State; - public static final field SUSPENDED Lkotlinx/coroutines/debug/State; - public static fun valueOf (Ljava/lang/String;)Lkotlinx/coroutines/debug/State; - public static fun values ()[Lkotlinx/coroutines/debug/State; -} - public synthetic class kotlinx/coroutines/debug/internal/DebugProbesImplSequenceNumberRefVolatile { public fun (J)V } diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfo.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfo.kt new file mode 100644 index 0000000000..82e18eabe7 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfo.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug.internal + +import kotlin.coroutines.* +import kotlin.coroutines.jvm.internal.* + +internal const val CREATED = "CREATED" +internal const val RUNNING = "RUNNING" +internal const val SUSPENDED = "SUSPENDED" + +internal class DebugCoroutineInfo( + public val context: CoroutineContext, + public val creationStackBottom: CoroutineStackFrame?, + @JvmField internal val sequenceNumber: Long +) { + + public val creationStackTrace: List get() = creationStackTrace() + + /** + * Last observed state of the coroutine. + * Can be CREATED, RUNNING, SUSPENDED. + */ + public val state: String get() = _state + private var _state: String = CREATED + + @JvmField + internal var lastObservedThread: Thread? = null + @JvmField + internal var lastObservedFrame: CoroutineStackFrame? = null + + public fun copy(): DebugCoroutineInfo = DebugCoroutineInfo( + context, + creationStackBottom, + sequenceNumber + ).also { + it._state = _state + it.lastObservedFrame = lastObservedFrame + it.lastObservedThread = lastObservedThread + } + + /** + * Last observed stacktrace of the coroutine captured on its suspension or resumption point. + * It means that for [running][State.RUNNING] coroutines resulting stacktrace is inaccurate and + * reflects stacktrace of the resumption point, not the actual current stacktrace. + */ + public fun lastObservedStackTrace(): List { + var frame: CoroutineStackFrame? = lastObservedFrame ?: return emptyList() + val result = ArrayList() + while (frame != null) { + frame.getStackTraceElement()?.let { result.add(it) } + frame = frame.callerFrame + } + return result + } + + private fun creationStackTrace(): List { + val bottom = creationStackBottom ?: return emptyList() + // Skip "Coroutine creation stacktrace" frame + return sequence { yieldFrames(bottom.callerFrame) }.toList() + } + + private tailrec suspend fun SequenceScope.yieldFrames(frame: CoroutineStackFrame?) { + if (frame == null) return + frame.getStackTraceElement()?.let { yield(it) } + val caller = frame.callerFrame + if (caller != null) { + yieldFrames(caller) + } + } + + internal fun updateState(state: String, frame: Continuation<*>) { + // Propagate only duplicating transitions to running for KT-29997 + if (_state == state && state == SUSPENDED && lastObservedFrame != null) return + _state = state + lastObservedFrame = frame as? CoroutineStackFrame + lastObservedThread = if (state == RUNNING) { + Thread.currentThread() + } else { + null + } + } + + override fun toString(): String = "DebugCoroutineInfo(state=$state,context=$context)" +} diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt index 2b990f4088..dfa6b14c08 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt @@ -6,7 +6,6 @@ package kotlinx.coroutines.debug.internal import kotlinx.atomicfu.* import kotlinx.coroutines.* -import kotlinx.coroutines.debug.* import java.io.* import java.text.* import java.util.* @@ -18,11 +17,6 @@ import kotlin.coroutines.* import kotlin.coroutines.jvm.internal.* import kotlinx.coroutines.internal.artificialFrame as createArtificialFrame // IDEA bug workaround -/** - * Mirror of [DebugProbes] with actual implementation. - * [DebugProbes] are implemented with pimpl to simplify user-facing class and make it look simple and - * documented. - */ internal object DebugProbesImpl { private const val ARTIFICIAL_FRAME_MESSAGE = "Coroutine creation stacktrace" private val dateFormat = SimpleDateFormat("yyyy/MM/dd HH:mm:ss") @@ -52,7 +46,7 @@ internal object DebugProbesImpl { * Then at least three RUNNING -> RUNNING transitions will occur consecutively and complexity of each is O(depth). * To avoid that quadratic complexity, we are caching lookup result for such chains in this map and update it incrementally. */ - private val callerInfoCache = ConcurrentHashMap() + private val callerInfoCache = ConcurrentHashMap() public fun install(): Unit = coroutineStateLock.write { if (++installations > 1) return @@ -77,7 +71,7 @@ internal object DebugProbesImpl { } } - private fun Job.build(map: Map, builder: StringBuilder, indent: String) { + private fun Job.build(map: Map, builder: StringBuilder, indent: String) { val info = map[this] val newIndent: String if (info == null) { // Append coroutine without stacktrace @@ -105,7 +99,7 @@ internal object DebugProbesImpl { @Suppress("DEPRECATION_ERROR") // JobSupport private val Job.debugString: String get() = if (this is JobSupport) toDebugString() else toString() - public fun dumpCoroutinesInfo(): List = coroutineStateLock.write { + public fun dumpCoroutinesInfo(): List = coroutineStateLock.write { check(isInstalled) { "Debug probes are not installed" } return capturedCoroutines.asSequence() .map { it.info.copy() } // Copy as CoroutineInfo can be mutated concurrently by DebugProbes @@ -113,6 +107,8 @@ internal object DebugProbesImpl { .toList() } + public fun dumpDebuggerInfo() = dumpCoroutinesInfo().map { DebuggerInfo(it) } + public fun dumpCoroutines(out: PrintStream): Unit = synchronized(out) { /* * This method synchronizes both on `out` and `this` for a reason: @@ -134,7 +130,7 @@ internal object DebugProbesImpl { val info = owner.info val observedStackTrace = info.lastObservedStackTrace() val enhancedStackTrace = enhanceStackTraceWithThreadDump(info, observedStackTrace) - val state = if (info.state == State.RUNNING && enhancedStackTrace === observedStackTrace) + val state = if (info.state == RUNNING && enhancedStackTrace === observedStackTrace) "${info.state} (Last suspension stacktrace, not an actual stacktrace)" else info.state.toString() @@ -156,17 +152,17 @@ internal object DebugProbesImpl { } /** - * Tries to enhance [coroutineTrace] (obtained by call to [CoroutineInfo.lastObservedStackTrace]) with - * thread dump of [CoroutineInfo.lastObservedThread]. + * Tries to enhance [coroutineTrace] (obtained by call to [DebugCoroutineInfo.lastObservedStackTrace]) with + * thread dump of [DebugCoroutineInfo.lastObservedThread]. * * Returns [coroutineTrace] if enhancement was unsuccessful or the enhancement result. */ private fun enhanceStackTraceWithThreadDump( - info: CoroutineInfo, + info: DebugCoroutineInfo, coroutineTrace: List ): List { val thread = info.lastObservedThread - if (info.state != State.RUNNING || thread == null) return coroutineTrace + if (info.state != RUNNING || thread == null) return coroutineTrace // Avoid security manager issues val actualTrace = runCatching { thread.stackTrace }.getOrNull() ?: return coroutineTrace @@ -250,13 +246,13 @@ internal object DebugProbesImpl { } } - internal fun probeCoroutineResumed(frame: Continuation<*>) = updateState(frame, State.RUNNING) + internal fun probeCoroutineResumed(frame: Continuation<*>) = updateState(frame, RUNNING) - internal fun probeCoroutineSuspended(frame: Continuation<*>) = updateState(frame, State.SUSPENDED) + internal fun probeCoroutineSuspended(frame: Continuation<*>) = updateState(frame, SUSPENDED) - private fun updateState(frame: Continuation<*>, state: State) { + private fun updateState(frame: Continuation<*>, state: String) { // KT-29997 is here only since 1.3.30 - if (state == State.RUNNING && KotlinVersion.CURRENT.isAtLeast(1, 3, 30)) { + if (state == RUNNING && KotlinVersion.CURRENT.isAtLeast(1, 3, 30)) { val stackFrame = frame as? CoroutineStackFrame ?: return updateRunningState(stackFrame, state) return @@ -268,10 +264,10 @@ internal object DebugProbesImpl { } // See comment to callerInfoCache - private fun updateRunningState(frame: CoroutineStackFrame, state: State): Unit = coroutineStateLock.read { + private fun updateRunningState(frame: CoroutineStackFrame, state: String): Unit = coroutineStateLock.read { if (!isInstalled) return // Lookup coroutine info in cache or by traversing stack frame - val info: CoroutineInfo + val info: DebugCoroutineInfo val cached = callerInfoCache.remove(frame) if (cached != null) { info = cached @@ -293,7 +289,7 @@ internal object DebugProbesImpl { return if (caller.getStackTraceElement() != null) caller else caller.realCaller() } - private fun updateState(owner: CoroutineOwner<*>, frame: Continuation<*>, state: State) = coroutineStateLock.read { + private fun updateState(owner: CoroutineOwner<*>, frame: Continuation<*>, state: String) = coroutineStateLock.read { if (!isInstalled) return owner.info.updateState(state, frame) } @@ -336,7 +332,7 @@ internal object DebugProbesImpl { private fun createOwner(completion: Continuation, frame: CoroutineStackFrame?): Continuation { if (!isInstalled) return completion - val info = CoroutineInfo(completion.context, frame, sequenceNumber.incrementAndGet()) + val info = DebugCoroutineInfo(completion.context, frame, sequenceNumber.incrementAndGet()) val owner = CoroutineOwner(completion, info, frame) capturedCoroutines += owner if (!isInstalled) capturedCoroutines.clear() @@ -360,7 +356,7 @@ internal object DebugProbesImpl { */ private class CoroutineOwner( @JvmField val delegate: Continuation, - @JvmField val info: CoroutineInfo, + @JvmField val info: DebugCoroutineInfo, private val frame: CoroutineStackFrame? ) : Continuation by delegate, CoroutineStackFrame { diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebuggerInfo.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebuggerInfo.kt new file mode 100644 index 0000000000..2b51aaf549 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebuggerInfo.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("PropertyName", "NO_EXPLICIT_VISIBILITY_IN_API_MODE") + +package kotlinx.coroutines.debug.internal + +import java.io.Serializable +import kotlin.coroutines.* +import kotlin.coroutines.jvm.internal.* + +internal class DebuggerInfo(source: DebugCoroutineInfo) : Serializable { + public val name: String? = source.context[kotlinx.coroutines.CoroutineName]?.name + public val state: String = source.state + public val lastObservedThreadState = source.lastObservedThread?.state + public val lastObservedThreadName = source.lastObservedThread?.name + public val lastObservedStackTrace = source.lastObservedStackTrace() + public val sequenceNumber = source.sequenceNumber +} diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DynamicAttach.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DynamicAttach.kt index 498613102f..8289d5c9a8 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/internal/DynamicAttach.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DynamicAttach.kt @@ -16,3 +16,4 @@ private fun getFunction(clzName: String): Function0? = runCatching { val ctor = clz.constructors[0] ctor.newInstance() as Function0 }.getOrNull() + diff --git a/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api b/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api index 3020519b84..b6056c410c 100644 --- a/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api +++ b/kotlinx-coroutines-debug/api/kotlinx-coroutines-debug.api @@ -1,3 +1,12 @@ +public final class kotlinx/coroutines/debug/CoroutineInfo { + public final fun getContext ()Lkotlin/coroutines/CoroutineContext; + public final fun getCreationStackTrace ()Ljava/util/List; + public final fun getJob ()Lkotlinx/coroutines/Job; + public final fun getState ()Lkotlinx/coroutines/debug/State; + public final fun lastObservedStackTrace ()Ljava/util/List; + public fun toString ()Ljava/lang/String; +} + public final class kotlinx/coroutines/debug/CoroutinesBlockHoundIntegration : reactor/blockhound/integration/BlockHoundIntegration { public fun ()V public fun applyTo (Lreactor/blockhound/BlockHound$Builder;)V @@ -24,6 +33,14 @@ public final class kotlinx/coroutines/debug/DebugProbes { public final fun withDebugProbes (Lkotlin/jvm/functions/Function0;)V } +public final class kotlinx/coroutines/debug/State : java/lang/Enum { + public static final field CREATED Lkotlinx/coroutines/debug/State; + public static final field RUNNING Lkotlinx/coroutines/debug/State; + public static final field SUSPENDED Lkotlinx/coroutines/debug/State; + public static fun valueOf (Ljava/lang/String;)Lkotlinx/coroutines/debug/State; + public static fun values ()[Lkotlinx/coroutines/debug/State; +} + public final class kotlinx/coroutines/debug/junit4/CoroutinesTimeout : org/junit/rules/TestRule { public static final field Companion Lkotlinx/coroutines/debug/junit4/CoroutinesTimeout$Companion; public fun (JZ)V diff --git a/kotlinx-coroutines-core/jvm/src/debug/CoroutineInfo.kt b/kotlinx-coroutines-debug/src/CoroutineInfo.kt similarity index 62% rename from kotlinx-coroutines-core/jvm/src/debug/CoroutineInfo.kt rename to kotlinx-coroutines-debug/src/CoroutineInfo.kt index a3dc74fe48..b290cdee99 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/CoroutineInfo.kt +++ b/kotlinx-coroutines-debug/src/CoroutineInfo.kt @@ -1,28 +1,27 @@ /* * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ - -@file:Suppress("PropertyName") - +@file:Suppress("NO_EXPLICIT_VISIBILITY_IN_API_MODE", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "UNUSED") package kotlinx.coroutines.debug import kotlinx.coroutines.* +import kotlinx.coroutines.debug.internal.* import kotlin.coroutines.* import kotlin.coroutines.jvm.internal.* /** * Class describing coroutine info such as its context, state and stacktrace. - * This API is usable only along with `kotlinx-coroutines-debug` module. */ -@ExperimentalCoroutinesApi -public class CoroutineInfo internal constructor( +public class CoroutineInfo internal constructor(delegate: DebugCoroutineInfo) { + /** + * [Coroutine context][coroutineContext] of the coroutine + */ + public val context: CoroutineContext = delegate.context /** - * Coroutine context of the coroutine + * Last observed state of the coroutine */ - public val context: CoroutineContext, - private val creationStackBottom: CoroutineStackFrame?, - @JvmField internal val sequenceNumber: Long -) { + public val state: State = State.valueOf(delegate.state) + private val creationStackBottom: CoroutineStackFrame? = delegate.creationStackBottom /** * [Job] associated with a current coroutine or null. @@ -36,29 +35,7 @@ public class CoroutineInfo internal constructor( */ public val creationStackTrace: List get() = creationStackTrace() - /** - * Last observed [state][State] of the coroutine. - */ - public val state: State get() = _state - - private var _state: State = - State.CREATED - - @JvmField - internal var lastObservedThread: Thread? = null - - @JvmField - internal var lastObservedFrame: CoroutineStackFrame? = null - - public fun copy(): CoroutineInfo = CoroutineInfo( - context, - creationStackBottom, - sequenceNumber - ).also { - it._state = _state - it.lastObservedFrame = lastObservedFrame - it.lastObservedThread = lastObservedThread - } + private val lastObservedFrame: CoroutineStackFrame? = delegate.lastObservedFrame /** * Last observed stacktrace of the coroutine captured on its suspension or resumption point. @@ -90,18 +67,6 @@ public class CoroutineInfo internal constructor( } } - internal fun updateState(state: State, frame: Continuation<*>) { - // Propagate only duplicating transitions to running for KT-29997 - if (_state == state && state == State.SUSPENDED && lastObservedFrame != null) return - _state = state - lastObservedFrame = frame as? CoroutineStackFrame - if (state == State.RUNNING) { - lastObservedThread = Thread.currentThread() - } else { - lastObservedThread = null - } - } - override fun toString(): String = "CoroutineInfo(state=$state,context=$context)" } diff --git a/kotlinx-coroutines-debug/src/DebugProbes.kt b/kotlinx-coroutines-debug/src/DebugProbes.kt index 4a828ecab9..254385f942 100644 --- a/kotlinx-coroutines-debug/src/DebugProbes.kt +++ b/kotlinx-coroutines-debug/src/DebugProbes.kt @@ -121,7 +121,7 @@ public object DebugProbes { * 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 = DebugProbesImpl.dumpCoroutinesInfo() + public fun dumpCoroutinesInfo(): List = 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. diff --git a/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt b/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt index 85aa657be4..14851b4855 100644 --- a/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt +++ b/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt @@ -1,9 +1,11 @@ /* * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") package kotlinx.coroutines.debug import kotlinx.coroutines.* +import kotlinx.coroutines.debug.internal.* import org.junit.Test import java.util.concurrent.* import kotlin.test.* @@ -175,10 +177,10 @@ class RunningThreadStackMergeTest : DebugTestBase() { fun testActiveThread() = runBlocking { launchCoroutine() awaitCoroutineStarted() - val info = DebugProbes.dumpCoroutinesInfo().find { it.state == State.RUNNING } + val info = DebugProbesImpl.dumpDebuggerInfo().find { it.state == "RUNNING" } assertNotNull(info) @Suppress("INVISIBLE_MEMBER") // IDEA bug - assertNotNull(info.lastObservedThread) + assertNotNull(info.lastObservedThreadName) coroutineBlocker.await() } } From fdb01d02a3c9bbdcf77ae9b70aa4509a43772a8a Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 21 Apr 2020 18:03:32 +0300 Subject: [PATCH 3/7] Tweak DebuggerInfo --- .../jvm/src/debug/internal/DebugProbesImpl.kt | 4 ++++ .../jvm/src/debug/internal/DebuggerInfo.kt | 18 ++++++++++++------ .../test/RunningThreadStackMergeTest.kt | 1 - 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt index dfa6b14c08..fc004f23ab 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt @@ -107,6 +107,10 @@ internal object DebugProbesImpl { .toList() } + /* + * Internal (JVM-public) method used by IDEA debugger. + * It is equivalent to dumpCoroutines, but returns serializable (and thus less typed) objects. + */ public fun dumpDebuggerInfo() = dumpCoroutinesInfo().map { DebuggerInfo(it) } public fun dumpCoroutines(out: PrintStream): Unit = synchronized(out) { diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebuggerInfo.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebuggerInfo.kt index 2b51aaf549..4b95af986a 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/internal/DebuggerInfo.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebuggerInfo.kt @@ -2,19 +2,25 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -@file:Suppress("PropertyName", "NO_EXPLICIT_VISIBILITY_IN_API_MODE") +@file:Suppress("UNUSED") package kotlinx.coroutines.debug.internal import java.io.Serializable import kotlin.coroutines.* -import kotlin.coroutines.jvm.internal.* +import kotlinx.coroutines.* +/* + * This class represents all the data required by IDEA debugger. + * It is serializable in order to speedup JDWP interactions + */ internal class DebuggerInfo(source: DebugCoroutineInfo) : Serializable { - public val name: String? = source.context[kotlinx.coroutines.CoroutineName]?.name + public val coroutineId: Long? = source.context[CoroutineId]?.id + public val dispatcher: String? = source.context[ContinuationInterceptor].toString() + public val name: String? = source.context[CoroutineName]?.name public val state: String = source.state - public val lastObservedThreadState = source.lastObservedThread?.state + public val lastObservedThreadState: String? = source.lastObservedThread?.state?.toString() public val lastObservedThreadName = source.lastObservedThread?.name - public val lastObservedStackTrace = source.lastObservedStackTrace() - public val sequenceNumber = source.sequenceNumber + public val lastObservedStackTrace: List = source.lastObservedStackTrace() + public val sequenceNumber: Long = source.sequenceNumber } diff --git a/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt b/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt index 14851b4855..4c13f5e67c 100644 --- a/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt +++ b/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt @@ -179,7 +179,6 @@ class RunningThreadStackMergeTest : DebugTestBase() { awaitCoroutineStarted() val info = DebugProbesImpl.dumpDebuggerInfo().find { it.state == "RUNNING" } assertNotNull(info) - @Suppress("INVISIBLE_MEMBER") // IDEA bug assertNotNull(info.lastObservedThreadName) coroutineBlocker.await() } From 428e6bc5fedde4ebb9947b834d186c07c9ac235c Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 21 Apr 2020 20:09:19 +0300 Subject: [PATCH 4/7] Introduce mechanism to produce a DebugProbesKt.bin and verify them against golden value --- DebugProbesKt.class | Bin 0 -> 1730 bytes integration-testing/build.gradle | 6 +++ .../src/debugAgentTest/kotlin/DebugProbes.kt | 14 +++++++ .../kotlin/PrecompiledDebugProbesTest.kt | 35 ++++++++++++++++++ .../jvm/resources/DebugProbesKt.bin | Bin 1628 -> 1730 bytes .../jvm/src/debug/AgentPremain.kt | 3 +- 6 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 DebugProbesKt.class create mode 100644 integration-testing/src/debugAgentTest/kotlin/DebugProbes.kt create mode 100644 integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt diff --git a/DebugProbesKt.class b/DebugProbesKt.class new file mode 100644 index 0000000000000000000000000000000000000000..8a7160fbad39f25ba582998c532c6ff65c788d5f GIT binary patch literal 1730 zcmb7ETTc@~7(KHsy{r|MdV`{<1r<q5Irc6N!6{2U+E z2V;CN#7BRW@tdVcDPT3}cIW%%>^a|U=I5_(-vQ+CgyGhv>$hyjv|P^(eA^My+}LiL zw&M%W;Vtu-s0Ymz&#epj%8!FF7#n<>n=S4%&E@)ru*e%>IJc!R#eJ5dC%7*f3{R)Z zM-_^$Ls9|vZP&?9_X99kh1#QXuWWv?A9Z22R?9E;6)1!#hKaK4HO&p-*FA1K(&Ucg z`eBUHthjz9XtnYTy5+XFT0(`MK$2m+lis1Kl^owA3fq!#oMO0Oy%W~v3io(hP+d#1 zL&tDq>S$hFo~oj0=BL+EIE_IaDGV{p?DaE7DB3uN84{&RwYF3#K4)0y zl>9#^@+q9b*(B09$1ryYd4jN z;d(z*VTLAxfCFz;Hxd{>l1@kQ*OBmHHP8+ zBE1$qZ*ZT}*lF$U2%TA`{tPnfp$TQu>0_H}p^?8a&*1Lvr1jk$T^rGKEfLW*BYbDF z#_mogk&dRdm)ac5MiaX`hL%eikpt&TLYy_C8EtMLryH?MEKLM+Y|kA(#{J_%ETLp` z6hw_tfXJ%5qv?eQXVw>zPI~iAIG;;R;rKQ3FbsCxE4Mev5iPn6!7x;&E2t8*>%yz? zdW$^ivTO0y8ux5vcbv(p-E_Dgcmx<*4d^y$i;}Z#OPerD2j`YyvUm5-DLe$zt8U;~ z;)SgwM?2rF9pS?~G(@S?RCSDE05P)ell6f7lsyJ#()w2zpK*TX3r0VZN25Iuj?tsz zA+1w;NFrse_Bi}(K=v|;EP6dTBMlptqB-bNz`HxKY6QR0gQio*5yJRl)QcwJy z`g$+*;J;F9cpOG`4~z6(rd)1Nxu(_;DPcPN&EV#~b*qF~+%Cby+&Y+~D(BXblo*i6 SNGwRT9+S8uF)lG7arZY>jlv25 literal 0 HcmV?d00001 diff --git a/integration-testing/build.gradle b/integration-testing/build.gradle index a7e3333fd5..3000318fdb 100644 --- a/integration-testing/build.gradle +++ b/integration-testing/build.gradle @@ -33,6 +33,12 @@ sourceSets { } } +compileDebugAgentTestKotlin { + kotlinOptions { + freeCompilerArgs += ["-Xallow-kotlin-package"] + } +} + task npmTest(type: Test) { def sourceSet = sourceSets.npmTest environment "projectRoot", project.rootDir diff --git a/integration-testing/src/debugAgentTest/kotlin/DebugProbes.kt b/integration-testing/src/debugAgentTest/kotlin/DebugProbes.kt new file mode 100644 index 0000000000..46a7634a8b --- /dev/null +++ b/integration-testing/src/debugAgentTest/kotlin/DebugProbes.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +package kotlin.coroutines.jvm.internal + +import kotlinx.coroutines.debug.internal.* +import kotlin.coroutines.* + +internal fun probeCoroutineCreated(completion: Continuation): Continuation = DebugProbesImpl.probeCoroutineCreated(completion) + +internal fun probeCoroutineResumed(frame: Continuation<*>) = DebugProbesImpl.probeCoroutineResumed(frame) + +internal fun probeCoroutineSuspended(frame: Continuation<*>) = DebugProbesImpl.probeCoroutineSuspended(frame) \ No newline at end of file diff --git a/integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt b/integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt new file mode 100644 index 0000000000..a36948c1ff --- /dev/null +++ b/integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +import org.junit.Test +import kotlin.test.* + +/* + * This is intentionally put here instead of coreAgentTest to avoid accidental classpath replacing + * and ruining core agent test. + */ +class PrecompiledDebugProbesTest { + + @Test + fun testClassFileContent() { + val clz = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") + val className: String = clz.getName() + val classFileResourcePath = className.replace(".", "/") + ".class" + val stream = clz.classLoader.getResourceAsStream(classFileResourcePath)!! + val array = stream.readBytes() + val binFile = clz.classLoader.getResourceAsStream("DebugProbesKt.bin")!! + val binContent = binFile.readBytes() + assertTrue(array.contentEquals(binContent)) + } + + private fun diffpos(a: ByteArray, b: ByteArray, typeLenght: Int): Int { + if (a.size == b.size) { + for (i in a.indices) { + if (a[i] != b[i]) { + println(i) + } + } + } + return -1 + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin b/kotlinx-coroutines-core/jvm/resources/DebugProbesKt.bin index 0787b8ce128b9fefb4f23becc8470a4c3780d58b..8a7160fbad39f25ba582998c532c6ff65c788d5f 100644 GIT binary patch delta 674 zcmaJ-%Wl&^6g^`%iQ_r}3Z*n{)6$ea0>LCupro`sR+eZA0(Fx` zyQ`hVsWe6~D!;SJ@EDRhfiny*6(Bd<&*F8nHtnC{8tYgIb_s|6kC0w*%;!+)qql`)&X^sR(j^i?q`w5OK9C?lc$MSDlo^=rb delta 560 zcmZ`#yG{Z@6g{&G0_*Zpo+=L?D5AXaNukk3qS3;V%8)f#g5DNNBbkZsVDdzcrKvG&k>WCQR2DWt$`l_j#kVX8&-LA^J+W4 zbx=qc<-{7_Ho_SLgb_ptBA<{kr9P*rPMedxR<1s)NRL%`P&GPCb-FVk>K!gx?^hMD zZuaJ}!Q&DC!3?aJtwO9|&Rhx1|D%Npl31)Dg{3M8#WK=WV+K}=07ZtvPZ6ZZQbZ`C G6uCdG8c*N= diff --git a/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt index 6ce9bbb3fe..766ca7812f 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt @@ -41,7 +41,8 @@ internal object AgentPremain { * DebugProbesKt.bin contains `kotlin.coroutines.jvm.internal.DebugProbesKt` class * with method bodies that delegate all calls directly to their counterparts in * kotlinx.coroutines.debug.DebugProbesImpl. This is done to avoid classfile patching - * on the fly (-> get rid of ASM dependency) + * on the fly (-> get rid of ASM dependency). + * You can verify its content either by using javap on it or looking at out integration test module. */ return loader.getResourceAsStream("DebugProbesKt.bin").readBytes() } From ea966ae58ee0b253ad36b57d620a54cc962e0e39 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 22 Apr 2020 17:54:57 +0300 Subject: [PATCH 5/7] ~various cleanups --- DebugProbesKt.class | Bin 1730 -> 0 bytes integration-testing/build.gradle | 2 +- .../kotlin/PrecompiledDebugProbesTest.kt | 26 ++++++++++-------- .../jvm/src/debug/AgentPremain.kt | 2 +- .../jvm/src/debug/internal/DebugProbesImpl.kt | 15 ++++++++-- .../jvm/src/debug/internal/DynamicAttach.kt | 19 ------------- .../src/internal/Attach.kt | 16 ++++++----- 7 files changed, 39 insertions(+), 41 deletions(-) delete mode 100644 DebugProbesKt.class delete mode 100644 kotlinx-coroutines-core/jvm/src/debug/internal/DynamicAttach.kt diff --git a/DebugProbesKt.class b/DebugProbesKt.class deleted file mode 100644 index 8a7160fbad39f25ba582998c532c6ff65c788d5f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1730 zcmb7ETTc@~7(KHsy{r|MdV`{<1r<q5Irc6N!6{2U+E z2V;CN#7BRW@tdVcDPT3}cIW%%>^a|U=I5_(-vQ+CgyGhv>$hyjv|P^(eA^My+}LiL zw&M%W;Vtu-s0Ymz&#epj%8!FF7#n<>n=S4%&E@)ru*e%>IJc!R#eJ5dC%7*f3{R)Z zM-_^$Ls9|vZP&?9_X99kh1#QXuWWv?A9Z22R?9E;6)1!#hKaK4HO&p-*FA1K(&Ucg z`eBUHthjz9XtnYTy5+XFT0(`MK$2m+lis1Kl^owA3fq!#oMO0Oy%W~v3io(hP+d#1 zL&tDq>S$hFo~oj0=BL+EIE_IaDGV{p?DaE7DB3uN84{&RwYF3#K4)0y zl>9#^@+q9b*(B09$1ryYd4jN z;d(z*VTLAxfCFz;Hxd{>l1@kQ*OBmHHP8+ zBE1$qZ*ZT}*lF$U2%TA`{tPnfp$TQu>0_H}p^?8a&*1Lvr1jk$T^rGKEfLW*BYbDF z#_mogk&dRdm)ac5MiaX`hL%eikpt&TLYy_C8EtMLryH?MEKLM+Y|kA(#{J_%ETLp` z6hw_tfXJ%5qv?eQXVw>zPI~iAIG;;R;rKQ3FbsCxE4Mev5iPn6!7x;&E2t8*>%yz? zdW$^ivTO0y8ux5vcbv(p-E_Dgcmx<*4d^y$i;}Z#OPerD2j`YyvUm5-DLe$zt8U;~ z;)SgwM?2rF9pS?~G(@S?RCSDE05P)ell6f7lsyJ#()w2zpK*TX3r0VZN25Iuj?tsz zA+1w;NFrse_Bi}(K=v|;EP6dTBMlptqB-bNz`HxKY6QR0gQio*5yJRl)QcwJy z`g$+*;J;F9cpOG`4~z6(rd)1Nxu(_;DPcPN&EV#~b*qF~+%Cby+&Y+~D(BXblo*i6 SNGwRT9+S8uF)lG7arZY>jlv25 diff --git a/integration-testing/build.gradle b/integration-testing/build.gradle index 3000318fdb..90f3365b09 100644 --- a/integration-testing/build.gradle +++ b/integration-testing/build.gradle @@ -96,6 +96,6 @@ compileTestKotlin { kotlinOptions.jvmTarget = "1.8" } -test { +check { dependsOn([npmTest, mavenTest, debugAgentTest, coreAgentTest]) } diff --git a/integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt b/integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt index a36948c1ff..5d799ee02f 100644 --- a/integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt +++ b/integration-testing/src/debugAgentTest/kotlin/PrecompiledDebugProbesTest.kt @@ -2,6 +2,7 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ import org.junit.Test +import java.io.* import kotlin.test.* /* @@ -10,6 +11,8 @@ import kotlin.test.* */ class PrecompiledDebugProbesTest { + private val overwrite = java.lang.Boolean.getBoolean("overwrite.probes") + @Test fun testClassFileContent() { val clz = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") @@ -19,17 +22,18 @@ class PrecompiledDebugProbesTest { val array = stream.readBytes() val binFile = clz.classLoader.getResourceAsStream("DebugProbesKt.bin")!! val binContent = binFile.readBytes() - assertTrue(array.contentEquals(binContent)) - } - - private fun diffpos(a: ByteArray, b: ByteArray, typeLenght: Int): Int { - if (a.size == b.size) { - for (i in a.indices) { - if (a[i] != b[i]) { - println(i) - } - } + if (overwrite) { + val url = clz.classLoader.getResource("DebugProbesKt.bin")!! + val base = url.toExternalForm().toString().removePrefix("jar:file:").substringBefore("/build") + val probes = File(base, "jvm/resources/DebugProbesKt.bin") + FileOutputStream(probes).use { it.write(array) } + println("Content was successfully overwritten!") + } else { + assertTrue( + array.contentEquals(binContent), + "Compiled DebugProbesKt.class does not match the file shipped as a resource in kotlinx-coroutines-core. " + + "Typically it happens because of the Kotlin version update (-> binary metadata). In that case, run the same test with -Poverwrite.probes=true and " + + "ensure that classfile has major version equal to 50 (Java 6 compliance)") } - return -1 } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt index 766ca7812f..1690be832e 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt @@ -56,7 +56,7 @@ internal object AgentPremain { // a real debug probes use-case, thus is not guarded against. DebugProbesImpl.dumpCoroutines(System.out) } else { - println("""Cannot perform coroutines dump, debug probes are disabled""") + println("Cannot perform coroutines dump, debug probes are disabled") } } } catch (t: Throwable) { diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt index fc004f23ab..ee7a68efcc 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt @@ -38,6 +38,17 @@ internal object DebugProbesImpl { public var sanitizeStackTraces: Boolean = true public var enableCreationStackTraces: Boolean = true + // Substitute for service loader, DI between core and debug modules + private val dynamicAttach = gettDynamicAttach() + + @Suppress("UNCHECKED_CAST") + private fun gettDynamicAttach(): Function1? = runCatching { + val clz = Class.forName("kotlinx.coroutines.debug.internal.ByteBuddyDynamicAttach") + val ctor = clz.constructors[0] + ctor.newInstance() as Function1 + }.getOrNull() + + /* * This is an optimization in the face of KT-29997: * Consider suspending call stack a()->b()->c() and c() completes its execution and every call is @@ -50,7 +61,7 @@ internal object DebugProbesImpl { public fun install(): Unit = coroutineStateLock.write { if (++installations > 1) return - attach.invoke() + dynamicAttach?.invoke(true) // attach } public fun uninstall(): Unit = coroutineStateLock.write { @@ -58,7 +69,7 @@ internal object DebugProbesImpl { if (--installations != 0) return capturedCoroutines.clear() callerInfoCache.clear() - detach.invoke() + dynamicAttach?.invoke(false) // detach } public fun hierarchyToString(job: Job): String = coroutineStateLock.write { diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DynamicAttach.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DynamicAttach.kt deleted file mode 100644 index 8289d5c9a8..0000000000 --- a/kotlinx-coroutines-core/jvm/src/debug/internal/DynamicAttach.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ -package kotlinx.coroutines.debug.internal - -private val noop = {} -/* - * Use dynamic attach if kotlinx-coroutines-debug is present in the classpath - */ -internal val attach = getFunction("kotlinx.coroutines.debug.internal.ByteBuddyDynamicAttach") ?: noop -internal val detach = getFunction("kotlinx.coroutines.debug.internal.ByteBuddyDynamicDetach") ?: noop - -@Suppress("UNCHECKED_CAST") -private fun getFunction(clzName: String): Function0? = runCatching { - val clz = Class.forName(clzName) - val ctor = clz.constructors[0] - ctor.newInstance() as Function0 -}.getOrNull() - diff --git a/kotlinx-coroutines-debug/src/internal/Attach.kt b/kotlinx-coroutines-debug/src/internal/Attach.kt index eedf7ad96f..cd4cc2a52e 100644 --- a/kotlinx-coroutines-debug/src/internal/Attach.kt +++ b/kotlinx-coroutines-debug/src/internal/Attach.kt @@ -9,11 +9,15 @@ import net.bytebuddy.agent.* import net.bytebuddy.dynamic.loading.* /* - * These classes are used reflectively from kotlinx-coroutines-core when this module is present in the classpath. - * It is a substitute for service loading + * This class is used reflectively from kotlinx-coroutines-core when this module is present in the classpath. + * It is a substitute for service loading. */ -internal class ByteBuddyDynamicAttach : Function0 { - override fun invoke() { +internal class ByteBuddyDynamicAttach : Function1 { + override fun invoke(value: Boolean) { + if (value) attach() else detach() + } + + private fun attach() { ByteBuddyAgent.install(ByteBuddyAgent.AttachmentProvider.ForEmulatedAttachment.INSTANCE) val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") val cl2 = Class.forName("kotlinx.coroutines.debug.DebugProbesKt") @@ -24,10 +28,8 @@ internal class ByteBuddyDynamicAttach : Function0 { .make() .load(cl.classLoader, ClassReloadingStrategy.fromInstalledAgent()) } -} -internal class ByteBuddyDynamicDetach : Function0 { - override fun invoke() { + private fun detach() { val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") val cl2 = Class.forName("kotlinx.coroutines.debug.internal.NoOpProbesKt") ByteBuddy() From 2ea43dfec17be7da88a634d52747f6ff91d07583 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 22 Apr 2020 18:31:45 +0300 Subject: [PATCH 6/7] Prevent byte buddy instantiation when debug agent was installed statically in order to avoid reflective call to Installer.agentmain --- .../jvm/src/debug/AgentPremain.kt | 4 ++++ .../jvm/src/debug/internal/DebugProbesImpl.kt | 12 +++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt index 1690be832e..68f3a78fb6 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt @@ -13,12 +13,15 @@ import java.security.* @Suppress("unused") internal object AgentPremain { + public var isInstalledStatically = false + private val enableCreationStackTraces = System.getProperty("kotlinx.coroutines.debug.enable.creation.stack.trace")?.toBoolean() ?: DebugProbesImpl.enableCreationStackTraces @JvmStatic public fun premain(args: String?, instrumentation: Instrumentation) { + isInstalledStatically = true instrumentation.addTransformer(DebugProbesTransformer) DebugProbesImpl.enableCreationStackTraces = enableCreationStackTraces DebugProbesImpl.install() @@ -44,6 +47,7 @@ internal object AgentPremain { * on the fly (-> get rid of ASM dependency). * You can verify its content either by using javap on it or looking at out integration test module. */ + isInstalledStatically = true return loader.getResourceAsStream("DebugProbesKt.bin").readBytes() } } diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt index ee7a68efcc..6e8d19fb3f 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt @@ -6,6 +6,7 @@ package kotlinx.coroutines.debug.internal import kotlinx.atomicfu.* import kotlinx.coroutines.* +import kotlinx.coroutines.debug.* import java.io.* import java.text.* import java.util.* @@ -38,11 +39,14 @@ internal object DebugProbesImpl { public var sanitizeStackTraces: Boolean = true public var enableCreationStackTraces: Boolean = true - // Substitute for service loader, DI between core and debug modules - private val dynamicAttach = gettDynamicAttach() + /* + * Substitute for service loader, DI between core and debug modules. + * If the agent was installed via command line -javaagent parameter, do not use byte-byddy to avoud + */ + private val dynamicAttach = getDynamicAttach() @Suppress("UNCHECKED_CAST") - private fun gettDynamicAttach(): Function1? = runCatching { + private fun getDynamicAttach(): Function1? = runCatching { val clz = Class.forName("kotlinx.coroutines.debug.internal.ByteBuddyDynamicAttach") val ctor = clz.constructors[0] ctor.newInstance() as Function1 @@ -61,6 +65,7 @@ internal object DebugProbesImpl { public fun install(): Unit = coroutineStateLock.write { if (++installations > 1) return + if (AgentPremain.isInstalledStatically) return dynamicAttach?.invoke(true) // attach } @@ -69,6 +74,7 @@ internal object DebugProbesImpl { if (--installations != 0) return capturedCoroutines.clear() callerInfoCache.clear() + if (AgentPremain.isInstalledStatically) return dynamicAttach?.invoke(false) // detach } From 1f2982025d95a27d00d7360ffdcdebb1d6afdd53 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 24 Apr 2020 12:13:39 +0300 Subject: [PATCH 7/7] ~cleanup --- kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt | 3 +-- kotlinx-coroutines-debug/src/CoroutineInfo.kt | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt index 68f3a78fb6..6493077593 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt @@ -36,8 +36,7 @@ internal object AgentPremain { protectionDomain: ProtectionDomain, classfileBuffer: ByteArray? ): ByteArray? { - val name = className.replace("/", ".") - if (name != "kotlin.coroutines.jvm.internal.DebugProbesKt") { + if (className != "kotlin/coroutines/jvm/internal/DebugProbesKt") { return null } /* diff --git a/kotlinx-coroutines-debug/src/CoroutineInfo.kt b/kotlinx-coroutines-debug/src/CoroutineInfo.kt index b290cdee99..11224f53b3 100644 --- a/kotlinx-coroutines-debug/src/CoroutineInfo.kt +++ b/kotlinx-coroutines-debug/src/CoroutineInfo.kt @@ -12,6 +12,7 @@ import kotlin.coroutines.jvm.internal.* /** * Class describing coroutine info such as its context, state and stacktrace. */ +@ExperimentalCoroutinesApi public class CoroutineInfo internal constructor(delegate: DebugCoroutineInfo) { /** * [Coroutine context][coroutineContext] of the coroutine