Skip to content
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

Split package and coroutine debug agent API #3977

Draft
wants to merge 8 commits into
base: develop
Choose a base branch
from
2 changes: 1 addition & 1 deletion gradle.properties
Expand Up @@ -3,7 +3,7 @@
#

# Kotlin
version=1.7.2-SNAPSHOT
version=1.7.2-SNAPSHOT-newdebug
group=org.jetbrains.kotlinx
kotlin_version=1.9.21

Expand Down
2 changes: 1 addition & 1 deletion kotlinx-coroutines-core/build.gradle
Expand Up @@ -288,7 +288,7 @@ allMetadataJar { setupManifest(it) }

static def setupManifest(Jar jar) {
jar.manifest {
attributes "Premain-Class": "kotlinx.coroutines.debug.AgentPremain"
attributes "Premain-Class": "kotlinx.coroutines.debugging.AgentPremain"
attributes "Can-Retransform-Classes": "true"
}
}
Expand Down
Expand Up @@ -16,7 +16,7 @@
volatile <fields>;
}

# These classes are only required by kotlinx.coroutines.debug.AgentPremain, which is only loaded when
# These classes are only required by kotlinx.coroutines.debugging.AgentPremain, which is only loaded when
# kotlinx-coroutines-core is used as a Java agent, so these are not needed in contexts where ProGuard is used.
-dontwarn java.lang.instrument.ClassFileTransformer
-dontwarn sun.misc.SignalHandler
Expand Down
Expand Up @@ -12,7 +12,7 @@
volatile <fields>;
}

# These classes are only required by kotlinx.coroutines.debug.AgentPremain, which is only loaded when
# These classes are only required by kotlinx.coroutines.debugging.AgentPremain, which is only loaded when
# kotlinx-coroutines-core is used as a Java agent, so these are not needed in contexts where ProGuard is used.
-dontwarn java.lang.instrument.ClassFileTransformer
-dontwarn sun.misc.SignalHandler
Expand All @@ -24,4 +24,4 @@
-dontwarn java.lang.ClassValue

# An annotation used for build tooling, won't be directly accessed.
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
Expand Up @@ -16,7 +16,7 @@
volatile <fields>;
}

# These classes are only required by kotlinx.coroutines.debug.AgentPremain, which is only loaded when
# These classes are only required by kotlinx.coroutines.debugging.AgentPremain, which is only loaded when
# kotlinx-coroutines-core is used as a Java agent, so these are not needed in contexts where ProGuard is used.
-dontwarn java.lang.instrument.ClassFileTransformer
-dontwarn sun.misc.SignalHandler
Expand Down
Expand Up @@ -21,9 +21,12 @@ internal class DebugCoroutineInfo internal constructor(
) {
internal val creationStackBottom: CoroutineStackFrame? = source.creationStackBottom // field is used as of 1.4-M3
public val sequenceNumber: Long = source.sequenceNumber // field is used as of 1.4-M3
// ok, should be public, there is the same field in CoroutineInfo
public val creationStackTrace = source.creationStackTrace // getter is used as of 1.4-M3
public val state: String = source.state // getter is used as of 1.4-M3
// ok, may be public
public val lastObservedThread: Thread? = source.lastObservedThread // field is used as of 1.4-M3
// ok, may be public
public val lastObservedFrame: CoroutineStackFrame? = source.lastObservedFrame // field is used as of 1.4-M3
@get:JvmName("lastObservedStackTrace") // method with this name is used as of 1.4-M3
public val lastObservedStackTrace: List<StackTraceElement> = source.lastObservedStackTrace()
Expand Down
Expand Up @@ -37,6 +37,8 @@ internal class DebugCoroutineInfoImpl internal constructor(
get() = _context.get()

public val creationStackTrace: List<StackTraceElement> get() = creationStackTrace()

// todo: create enhancedStackTrace public field

/**
* Last observed state of the coroutine.
Expand Down
Expand Up @@ -10,13 +10,13 @@ import kotlinx.coroutines.internal.ScopeCoroutine
import java.io.*
import java.lang.StackTraceElement
import java.text.*
import java.util.concurrent.locks.*
import kotlin.collections.ArrayList
import kotlin.concurrent.*
import kotlin.coroutines.*
import kotlin.coroutines.jvm.internal.CoroutineStackFrame
import kotlin.synchronized
import _COROUTINE.ArtificialStackFrames
import kotlinx.coroutines.debugging.*

@PublishedApi
internal object DebugProbesImpl {
Expand Down Expand Up @@ -312,6 +312,7 @@ internal object DebugProbesImpl {
/*
* Internal (JVM-public) method used by IDEA debugger as of 1.4-M3, must be kept binary-compatible. See KTIJ-24102.
* It is similar to [enhanceStackTraceWithThreadDumpImpl], but uses debugger-facing [DebugCoroutineInfo] type.
* // TODO: may be moved to DebugCoroutineInfo, and exposed as a public field.
*/
@Suppress("unused")
fun enhanceStackTraceWithThreadDump(
Expand Down Expand Up @@ -529,13 +530,13 @@ internal object DebugProbesImpl {
}

/**
* This class is injected as completion of all continuations in [probeCoroutineCompleted].
* This class is injected as completion of all continuations in [probeCoroutineCompleted]. // todo fix in probeCoroutineCreated
* It is owning the coroutine info and responsible for managing all its external info related to debug agent.
*/
public class CoroutineOwner<T> internal constructor(
@JvmField internal val delegate: Continuation<T>,
// Used by the IDEA debugger via reflection and must be kept binary-compatible, see KTIJ-24102
@JvmField public val info: DebugCoroutineInfoImpl
@JvmField public val info: DebugCoroutineInfoImpl // TODO: should be DebugCoroutineInfo type
) : Continuation<T> by delegate, CoroutineStackFrame {
private val frame get() = info.creationStackBottom

Expand Down
Expand Up @@ -8,6 +8,7 @@ import kotlin.coroutines.jvm.internal.*

/**
* A stack-trace represented as [CoroutineStackFrame].
* It's not used in IDEA -> may be moved to the debugging package.
*/
@PublishedApi
internal class StackTraceFrame internal constructor(
Expand Down
Expand Up @@ -2,12 +2,12 @@
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.coroutines.debug.internal
package kotlinx.coroutines.debugging

/**
* Object used to differentiate between agent installed statically or dynamically.
* This is done in a separate object so [DebugProbesImpl] can check for static installation
* without having to depend on [kotlinx.coroutines.debug.AgentPremain], which is not compatible with Android.
* without having to depend on [kotlinx.coroutines.debugging.AgentPremain], which is not compatible with Android.
* Otherwise, access to `AgentPremain.isInstalledStatically` triggers the load of its internal `ClassFileTransformer`
* that is not available on Android.
*/
Expand Down
@@ -1,16 +1,16 @@
/*
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.coroutines.debug
package kotlinx.coroutines.debugging

import android.annotation.*
import kotlinx.coroutines.debug.internal.*
import org.codehaus.mojo.animal_sniffer.*
import sun.misc.*
import java.lang.instrument.*
import java.lang.instrument.ClassFileTransformer
import java.security.*
import kotlinx.coroutines.debug.internal.DebugProbesImpl

/*
* This class is loaded if and only if kotlinx-coroutines-core was used as -javaagent argument,
Expand Down
@@ -1,8 +1,8 @@
/*
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.coroutines.debug.internal
package kotlinx.coroutines.debugging

import kotlinx.atomicfu.*
import kotlinx.coroutines.internal.*
Expand Down
194 changes: 194 additions & 0 deletions kotlinx-coroutines-core/jvm/src/debugging/DebugCoroutineInfo.kt
@@ -0,0 +1,194 @@
/*
* Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.coroutines.debugging

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

// TODO: do we want to reuse DebugCoroutineInfo?
public class DebugCoroutineInfo internal constructor(
private val delegate: DebugCoroutineInfoImpl,
public val context: CoroutineContext // TODO: let's give out only necesssary fields
) {
/**
* [Job] associated with a current coroutine or null.
* TODO: may be extracted from context
*/
public val job: Job? get() = context[Job]

/**
* Last observed state of the coroutine
* // TODO: should pass enum value { CREATED, SUSPENDED, RUNNING }
* TODO: can change
*/
@JvmField
public val state: String = delegate.state

/**
* Last active thread used by a coroutine captured on its suspension or resumption point.
* TODO: can change
*/
@JvmField
public val lastObservedThread: Thread? = delegate.lastObservedThread

/**
* Last observed coroutine frame captured on its suspension or resumption point.
* // TODO: do not need it. enhancedStackTrace returns the list with both plain and coroutine frames
* TODO: can change
*/
@JvmField
public val lastObservedFrame: CoroutineStackFrame? = delegate.lastObservedFrame

/**
* Creation stacktrace of the coroutine.
*/
public val creationStackTrace: List<StackTraceElement> = delegate.creationStackTrace

/**
* TODO: Note, this implementation is copied from kotlinx.coroutines.debug.internal.DebugProbesImpl
*/
public fun enhanceStackTraceWithThreadDumpAsJson(): String {
val stackTraceElements = enhanceStackTraceWithThreadDumpImpl(state, lastObservedThread, delegate.lastObservedStackTrace())
val stackTraceElementsInfoAsJson = mutableListOf<String>()
for (element in stackTraceElements) {
stackTraceElementsInfoAsJson.add(
"""
{
"declaringClass": "${element.className}",
"methodName": "${element.methodName}",
"fileName": ${element.fileName?.toString()?.repr()},
"lineNumber": ${element.lineNumber}
}
""".trimIndent()
)
}

return "[${stackTraceElementsInfoAsJson.joinToString()}]"
}


// TODO: Note, this implementation is copied from kotlinx.coroutines.debug.internal.DebugProbesImpl
private fun String.repr(): String = buildString {
append('"')
for (c in this@repr) {
when (c) {
'"' -> append("\\\"")
'\\' -> append("\\\\")
'\b' -> append("\\b")
'\n' -> append("\\n")
'\r' -> append("\\r")
'\t' -> append("\\t")
else -> append(c)
}
}
append('"')
}

/**
* TODO: Note, this implementation is copied from kotlinx.coroutines.debug.internal.DebugProbesImpl
* Tries to enhance [coroutineTrace] (obtained by call to [DebugCoroutineInfoImpl.lastObservedStackTrace]) with
* thread dump of [DebugCoroutineInfoImpl.lastObservedThread].
*
* Returns [coroutineTrace] if enhancement was unsuccessful or the enhancement result.
*/
private fun enhanceStackTraceWithThreadDumpImpl(
state: String,
thread: Thread?,
coroutineTrace: List<StackTraceElement>
): List<StackTraceElement> {
if (state != RUNNING || thread == null) return coroutineTrace
// Avoid security manager issues
val actualTrace = runCatching { thread.stackTrace }.getOrNull()
?: return coroutineTrace

/*
* TODO: Note, this implementation is copied from kotlinx.coroutines.debug.internal.DebugProbesImpl
* Here goes heuristic that tries to merge two stacktraces: real one
* (that has at least one but usually not so many suspend function frames)
* and coroutine one that has only suspend function frames.
*
* Heuristic:
* 1) Dump lastObservedThread
* 2) Find the next frame after BaseContinuationImpl.resumeWith (continuation machinery).
* Invariant: this method is called under the lock, so such method **should** be present
* in continuation stacktrace.
* 3) Find target method in continuation stacktrace (metadata-based)
* 4) Prepend dumped stacktrace (trimmed by target frame) to continuation stacktrace
*
* Heuristic may fail on recursion and overloads, but it will be automatically improved
* with KT-29997.
*/
val indexOfResumeWith = actualTrace.indexOfFirst {
it.className == "kotlin.coroutines.jvm.internal.BaseContinuationImpl" &&
it.methodName == "resumeWith" &&
it.fileName == "ContinuationImpl.kt"
}

val (continuationStartFrame, delta) = findContinuationStartIndex(
indexOfResumeWith,
actualTrace,
coroutineTrace
)

if (continuationStartFrame == -1) return coroutineTrace

val expectedSize = indexOfResumeWith + coroutineTrace.size - continuationStartFrame - 1 - delta
val result = ArrayList<StackTraceElement>(expectedSize)
for (index in 0 until indexOfResumeWith - delta) {
result += actualTrace[index]
}

for (index in continuationStartFrame + 1 until coroutineTrace.size) {
result += coroutineTrace[index]
}

return result
}

/**
* TODO: Note, this implementation is copied from kotlinx.coroutines.debug.internal.DebugProbesImpl
* Tries to find the lowest meaningful frame above `resumeWith` in the real stacktrace and
* its match in a coroutines stacktrace (steps 2-3 in heuristic).
*
* This method does more than just matching `realTrace.indexOf(resumeWith) - 1`:
* If method above `resumeWith` has no line number (thus it is `stateMachine.invokeSuspend`),
* it's skipped and attempt to match next one is made because state machine could have been missing in the original coroutine stacktrace.
*
* Returns index of such frame (or -1) and number of skipped frames (up to 2, for state machine and for access$).
*/
private fun findContinuationStartIndex(
indexOfResumeWith: Int,
actualTrace: Array<StackTraceElement>,
coroutineTrace: List<StackTraceElement>
): Pair<Int, Int> {
/*
* Since Kotlin 1.5.0 we have these access$ methods that we have to skip.
* So we have to test next frame for invokeSuspend, for $access and for actual suspending call.
*/
repeat(3) {
val result = findIndexOfFrame(indexOfResumeWith - 1 - it, actualTrace, coroutineTrace)
if (result != -1) return result to it
}
return -1 to 0
}

// TODO: Note, this implementation is copied from kotlinx.coroutines.debug.internal.DebugProbesImpl
private fun findIndexOfFrame(
frameIndex: Int,
actualTrace: Array<StackTraceElement>,
coroutineTrace: List<StackTraceElement>
): Int {
val continuationFrame = actualTrace.getOrNull(frameIndex)
?: return -1

return coroutineTrace.indexOfFirst {
it.fileName == continuationFrame.fileName &&
it.className == continuationFrame.className &&
it.methodName == continuationFrame.methodName
}
}
}
@@ -1,11 +1,12 @@
/*
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2016-2023 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
package kotlinx.coroutines.debugging

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

/*
Expand Down