Skip to content

Extract debugger #1905

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

Merged
merged 7 commits into from
Apr 24, 2020
Merged
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
25 changes: 23 additions & 2 deletions integration-testing/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ sourceSets {
compileClasspath += sourceSets.test.runtimeClasspath
runtimeClasspath += sourceSets.test.runtimeClasspath
}

coreAgentTest {
kotlin
compileClasspath += sourceSets.test.runtimeClasspath
runtimeClasspath += sourceSets.test.runtimeClasspath
}
}

compileDebugAgentTestKotlin {
kotlinOptions {
freeCompilerArgs += ["-Xallow-kotlin-package"]
}
}

task npmTest(type: Test) {
Expand Down Expand Up @@ -62,19 +74,28 @@ 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'
npmTestCompile 'org.apache.commons:commons-compress:1.18'
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 {
kotlinOptions.jvmTarget = "1.8"
}

test {
dependsOn([npmTest, mavenTest, debugAgentTest])
check {
dependsOn([npmTest, mavenTest, debugAgentTest, coreAgentTest])
}
22 changes: 22 additions & 0 deletions integration-testing/src/coreAgentTest/kotlin/CoreAgentTest.kt
Original file line number Diff line number Diff line change
@@ -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"))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}

}
14 changes: 14 additions & 0 deletions integration-testing/src/debugAgentTest/kotlin/DebugProbes.kt
Original file line number Diff line number Diff line change
@@ -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 <T> probeCoroutineCreated(completion: Continuation<T>): Continuation<T> = DebugProbesImpl.probeCoroutineCreated(completion)

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

internal fun probeCoroutineSuspended(frame: Continuation<*>) = DebugProbesImpl.probeCoroutineSuspended(frame)
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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.*

/*
* This is intentionally put here instead of coreAgentTest to avoid accidental classpath replacing
* and ruining core agent test.
*/
class PrecompiledDebugProbesTest {

private val overwrite = java.lang.Boolean.getBoolean("overwrite.probes")

@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()
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)")
}
}
}
4 changes: 4 additions & 0 deletions kotlinx-coroutines-core/api/kotlinx-coroutines-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,10 @@ public final class kotlinx/coroutines/channels/ValueOrClosed {
public final synthetic fun unbox-impl ()Ljava/lang/Object;
}

public synthetic class kotlinx/coroutines/debug/internal/DebugProbesImplSequenceNumberRefVolatile {
public fun <init> (J)V
}

public abstract class kotlinx/coroutines/flow/AbstractFlow : kotlinx/coroutines/flow/Flow {
public fun <init> ()V
public final fun collect (Lkotlinx/coroutines/flow/FlowCollector;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
Expand Down
7 changes: 7 additions & 0 deletions kotlinx-coroutines-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
Binary file not shown.
69 changes: 69 additions & 0 deletions kotlinx-coroutines-core/jvm/src/debug/AgentPremain.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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 {

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()
installSignalHandler()
}

internal object DebugProbesTransformer : ClassFileTransformer {
override fun transform(
loader: ClassLoader,
className: String,
classBeingRedefined: Class<*>?,
protectionDomain: ProtectionDomain,
classfileBuffer: ByteArray?
): ByteArray? {
if (className != "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).
* 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()
}
}

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")
}
}
}
Original file line number Diff line number Diff line change
@@ -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<StackTraceElement> 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<StackTraceElement> {
var frame: CoroutineStackFrame? = lastObservedFrame ?: return emptyList()
val result = ArrayList<StackTraceElement>()
while (frame != null) {
frame.getStackTraceElement()?.let { result.add(it) }
frame = frame.callerFrame
}
return result
}

private fun creationStackTrace(): List<StackTraceElement> {
val bottom = creationStackBottom ?: return emptyList()
// Skip "Coroutine creation stacktrace" frame
return sequence<StackTraceElement> { yieldFrames(bottom.callerFrame) }.toList()
}

private tailrec suspend fun SequenceScope<StackTraceElement>.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)"
}
Loading