Introduce CoroutineStackFrame interface for coroutine stack reconstruction in debugger
Fixes KT-26339
This commit is contained in:
committed by
Ilya Gorbunov
parent
b23bf371cf
commit
a9eb94d822
@@ -8,6 +8,7 @@ import java.util.concurrent.atomic.AtomicReferenceFieldUpdater
|
|||||||
import kotlin.*
|
import kotlin.*
|
||||||
import kotlin.coroutines.intrinsics.CoroutineSingletons.*
|
import kotlin.coroutines.intrinsics.CoroutineSingletons.*
|
||||||
import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
|
import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
|
||||||
|
import kotlin.coroutines.jvm.internal.CoroutineStackFrame
|
||||||
|
|
||||||
@PublishedApi
|
@PublishedApi
|
||||||
@SinceKotlin("1.3")
|
@SinceKotlin("1.3")
|
||||||
@@ -15,7 +16,7 @@ internal actual class SafeContinuation<in T>
|
|||||||
internal actual constructor(
|
internal actual constructor(
|
||||||
private val delegate: Continuation<T>,
|
private val delegate: Continuation<T>,
|
||||||
initialResult: Any?
|
initialResult: Any?
|
||||||
) : Continuation<T> {
|
) : Continuation<T>, CoroutineStackFrame {
|
||||||
@PublishedApi
|
@PublishedApi
|
||||||
internal actual constructor(delegate: Continuation<T>) : this(delegate, UNDECIDED)
|
internal actual constructor(delegate: Continuation<T>) : this(delegate, UNDECIDED)
|
||||||
|
|
||||||
@@ -60,4 +61,15 @@ internal actual constructor(
|
|||||||
else -> result // either COROUTINE_SUSPENDED or data
|
else -> result // either COROUTINE_SUSPENDED or data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- CoroutineStackFrame implementation
|
||||||
|
|
||||||
|
public override val callerFrame: CoroutineStackFrame?
|
||||||
|
get() = delegate as? CoroutineStackFrame
|
||||||
|
|
||||||
|
override fun getStackTraceElement(): StackTraceElement? =
|
||||||
|
null
|
||||||
|
|
||||||
|
override fun toString(): String =
|
||||||
|
"SafeContinuation for $delegate"
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-4
@@ -16,7 +16,7 @@ internal abstract class BaseContinuationImpl(
|
|||||||
// This is `public val` so that it is private on JVM and cannot be modified by untrusted code, yet
|
// This is `public val` so that it is private on JVM and cannot be modified by untrusted code, yet
|
||||||
// it has a public getter (since even untrusted code is allowed to inspect its call stack).
|
// it has a public getter (since even untrusted code is allowed to inspect its call stack).
|
||||||
public val completion: Continuation<Any?>?
|
public val completion: Continuation<Any?>?
|
||||||
) : Continuation<Any?>, Serializable {
|
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
|
||||||
// This implementation is final. This fact is used to unroll resumeWith recursion.
|
// This implementation is final. This fact is used to unroll resumeWith recursion.
|
||||||
public final override fun resumeWith(result: SuccessOrFailure<Any?>) {
|
public final override fun resumeWith(result: SuccessOrFailure<Any?>) {
|
||||||
// Invoke "resume" debug probe only once, even if previous frames are "resumed" in the loop below, too
|
// Invoke "resume" debug probe only once, even if previous frames are "resumed" in the loop below, too
|
||||||
@@ -63,9 +63,16 @@ internal abstract class BaseContinuationImpl(
|
|||||||
throw UnsupportedOperationException("create(Any?;Continuation) has not been overridden")
|
throw UnsupportedOperationException("create(Any?;Continuation) has not been overridden")
|
||||||
}
|
}
|
||||||
|
|
||||||
public override fun toString(): String {
|
public override fun toString(): String =
|
||||||
return "Continuation at ${getStackTraceElement() ?: this::class.java.name}"
|
"Continuation at ${getStackTraceElement() ?: this::class.java.name}"
|
||||||
}
|
|
||||||
|
// --- CoroutineStackFrame implementation
|
||||||
|
|
||||||
|
public override val callerFrame: CoroutineStackFrame?
|
||||||
|
get() = completion as? CoroutineStackFrame
|
||||||
|
|
||||||
|
public override fun getStackTraceElement(): StackTraceElement? =
|
||||||
|
getStackTraceElementImpl()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SinceKotlin("1.3")
|
@SinceKotlin("1.3")
|
||||||
|
|||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||||
|
* that can be found in the license/LICENSE.txt file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package kotlin.coroutines.jvm.internal
|
||||||
|
|
||||||
|
import kotlin.coroutines.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents one frame in the coroutine call stack for debugger.
|
||||||
|
* This interface is implemented by compiler-generated implementations of
|
||||||
|
* [Continuation] interface.
|
||||||
|
*/
|
||||||
|
public interface CoroutineStackFrame {
|
||||||
|
/**
|
||||||
|
* Returns a reference to the stack frame of the caller of this frame,
|
||||||
|
* that is a frame before this frame in coroutine call stack.
|
||||||
|
* The result is `null` for the first frame of coroutine.
|
||||||
|
*/
|
||||||
|
public val callerFrame: CoroutineStackFrame?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns stack trace element that correspond to this stack frame.
|
||||||
|
* The result is `null` if the stack trace element is not available for this frame.
|
||||||
|
* In this case, the debugger represents this stack frame using the
|
||||||
|
* result of [toString] function.
|
||||||
|
*/
|
||||||
|
public fun getStackTraceElement(): StackTraceElement?
|
||||||
|
}
|
||||||
+19
-12
@@ -31,26 +31,30 @@ internal annotation class DebugMetadata(
|
|||||||
* The coroutine can be either running coroutine, that calls the function on its continuation and obtaining
|
* The coroutine can be either running coroutine, that calls the function on its continuation and obtaining
|
||||||
* the information about current file and line number, or, more likely, the function is called to produce accurate stack traces of
|
* the information about current file and line number, or, more likely, the function is called to produce accurate stack traces of
|
||||||
* suspended coroutine.
|
* suspended coroutine.
|
||||||
|
*
|
||||||
|
* The result is `null` when debug metadata is not available.
|
||||||
*/
|
*/
|
||||||
@SinceKotlin("1.3")
|
@SinceKotlin("1.3")
|
||||||
@JvmName("getStackTraceElement")
|
@JvmName("getStackTraceElement")
|
||||||
internal fun BaseContinuationImpl.getStackTraceElement(): StackTraceElement? {
|
internal fun BaseContinuationImpl.getStackTraceElementImpl(): StackTraceElement? {
|
||||||
val debugMetadata = getDebugMetadataAnnotation()
|
val debugMetadata = getDebugMetadataAnnotation() ?: return null
|
||||||
val label = getLabel()
|
val label = getLabel()
|
||||||
val fileName = if (label < 0) "" else debugMetadata.sourceFiles[label]
|
val fileName = if (label < 0) "" else debugMetadata.sourceFiles[label]
|
||||||
val lineNumber = if (label < 0) -1 else debugMetadata.lineNumbers[label]
|
val lineNumber = if (label < 0) -1 else debugMetadata.lineNumbers[label]
|
||||||
return StackTraceElement(debugMetadata.className, debugMetadata.methodName, fileName, lineNumber)
|
return StackTraceElement(debugMetadata.className, debugMetadata.methodName, fileName, lineNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun BaseContinuationImpl.getDebugMetadataAnnotation(): DebugMetadata {
|
private fun BaseContinuationImpl.getDebugMetadataAnnotation(): DebugMetadata? =
|
||||||
return javaClass.annotations.filterIsInstance<DebugMetadata>()[0]
|
javaClass.getAnnotation(DebugMetadata::class.java)
|
||||||
}
|
|
||||||
|
|
||||||
private fun BaseContinuationImpl.getLabel(): Int {
|
private fun BaseContinuationImpl.getLabel(): Int =
|
||||||
val field = javaClass.getDeclaredField("label") ?: return -1
|
try {
|
||||||
field.isAccessible = true
|
val field = javaClass.getDeclaredField("label")
|
||||||
return (field.get(this) as? Int ?: 0) - 1
|
field.isAccessible = true
|
||||||
}
|
(field.get(this) as? Int ?: 0) - 1
|
||||||
|
} catch (e: Exception) { // NoSuchFieldException, SecurityException, or IllegalAccessException
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an array of spilled variable names and continuation's field names where the variable has been spilled.
|
* Returns an array of spilled variable names and continuation's field names where the variable has been spilled.
|
||||||
@@ -60,10 +64,13 @@ private fun BaseContinuationImpl.getLabel(): Int {
|
|||||||
*
|
*
|
||||||
* The function is for debugger to use, thus it returns simplest data type possible.
|
* The function is for debugger to use, thus it returns simplest data type possible.
|
||||||
* This function should only be called on suspended coroutines to get accurate mapping.
|
* This function should only be called on suspended coroutines to get accurate mapping.
|
||||||
|
*
|
||||||
|
* The result is `null` when debug metadata is not available.
|
||||||
*/
|
*/
|
||||||
@SinceKotlin("1.3")
|
@SinceKotlin("1.3")
|
||||||
internal fun BaseContinuationImpl.getSpilledVariableFieldMapping(): Array<String> {
|
@JvmName("getSpilledVariableFieldMapping")
|
||||||
val debugMetadata = getDebugMetadataAnnotation()
|
internal fun BaseContinuationImpl.getSpilledVariableFieldMapping(): Array<String>? {
|
||||||
|
val debugMetadata = getDebugMetadataAnnotation() ?: return null
|
||||||
val res = arrayListOf<String>()
|
val res = arrayListOf<String>()
|
||||||
val label = getLabel()
|
val label = getLabel()
|
||||||
for ((i, labelOfIndex) in debugMetadata.indexToLabel.withIndex()) {
|
for ((i, labelOfIndex) in debugMetadata.indexToLabel.withIndex()) {
|
||||||
|
|||||||
@@ -46,18 +46,18 @@ class DebugMetadataTest {
|
|||||||
StackTraceElement("SomeClass", "testMethod", "test.kt", 10),
|
StackTraceElement("SomeClass", "testMethod", "test.kt", 10),
|
||||||
myContinuation.getStackTraceElement()
|
myContinuation.getStackTraceElement()
|
||||||
)
|
)
|
||||||
assertEquals(listOf("L$1", "a", "L$2", "b"), myContinuation.getSpilledVariableFieldMapping().toList())
|
assertEquals(listOf("L$1", "a", "L$2", "b"), myContinuation.getSpilledVariableFieldMapping()!!.toList())
|
||||||
myContinuation.label = 2
|
myContinuation.label = 2
|
||||||
assertEquals(
|
assertEquals(
|
||||||
StackTraceElement("SomeClass", "testMethod", "test1.kt", 2),
|
StackTraceElement("SomeClass", "testMethod", "test1.kt", 2),
|
||||||
myContinuation.getStackTraceElement()
|
myContinuation.getStackTraceElement()
|
||||||
)
|
)
|
||||||
assertEquals(listOf("L$1", "b", "L$2", "c"), myContinuation.getSpilledVariableFieldMapping().toList())
|
assertEquals(listOf("L$1", "b", "L$2", "c"), myContinuation.getSpilledVariableFieldMapping()!!.toList())
|
||||||
myContinuation.label = 3
|
myContinuation.label = 3
|
||||||
assertEquals(
|
assertEquals(
|
||||||
StackTraceElement("SomeClass", "testMethod", "test.kt", 11),
|
StackTraceElement("SomeClass", "testMethod", "test.kt", 11),
|
||||||
myContinuation.getStackTraceElement()
|
myContinuation.getStackTraceElement()
|
||||||
)
|
)
|
||||||
assertEquals(listOf("L$1", "c"), myContinuation.getSpilledVariableFieldMapping().toList())
|
assertEquals(listOf("L$1", "c"), myContinuation.getSpilledVariableFieldMapping()!!.toList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user