Introduce CoroutineStackFrame interface for coroutine stack reconstruction in debugger

Fixes KT-26339
This commit is contained in:
Roman Elizarov
2018-08-26 23:37:35 +03:00
committed by Ilya Gorbunov
parent b23bf371cf
commit a9eb94d822
5 changed files with 76 additions and 20 deletions
@@ -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"
} }
@@ -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")
@@ -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?
}
@@ -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())
} }
} }