diff --git a/libraries/stdlib/coroutines/jvm/src/kotlin/coroutines/jvm/internal/DebugMetadata.kt b/libraries/stdlib/coroutines/jvm/src/kotlin/coroutines/jvm/internal/DebugMetadata.kt new file mode 100644 index 00000000000..b8b8adee056 --- /dev/null +++ b/libraries/stdlib/coroutines/jvm/src/kotlin/coroutines/jvm/internal/DebugMetadata.kt @@ -0,0 +1,77 @@ +/* + * 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.Continuation + +// TODO: Uncomment when KT-25372 is fixed +@Target(AnnotationTarget.CLASS) +@SinceKotlin("1.3") +internal annotation class DebugMetadata( + // @JvmName("r") + val sourceFiles: Array, + // @JvmName("l") + val lineNumbers: IntArray, + // @JvmName("n") + val localNames: Array, + // @JvmName("s") + val spilled: Array, + // @JvmName("i") + val indexToLabel: IntArray, + // @JvmName("m") + val methodName: String, + // @JvmName("c") + val className: String +) + +/** + * Returns [StackTraceElement] containing file name and line number of current coroutine's suspension point. + * 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 + * suspended coroutine. + */ +@SinceKotlin("1.3") +@JvmName("getStackTraceElement") +internal fun BaseContinuationImpl.getStackTraceElement(): StackTraceElement? { + val debugMetadata = getDebugMetadataAnnotation() + val label = getLabel() + val fileName = if (label < 0) "" else debugMetadata.sourceFiles[label] + val lineNumber = if (label < 0) -1 else debugMetadata.lineNumbers[label] + return StackTraceElement(debugMetadata.className, debugMetadata.methodName, fileName, lineNumber) +} + +private fun BaseContinuationImpl.getDebugMetadataAnnotation(): DebugMetadata { + return javaClass.annotations.filterIsInstance()[0] +} + +private fun BaseContinuationImpl.getLabel(): Int { + val field = javaClass.getDeclaredField("label") ?: return -1 + field.isAccessible = true + return (field.get(this) as? Int ?: 0) - 1 +} + +/** + * Returns an array of spilled variable names and continuation's field names where the variable has been spilled. + * The structure is the following: + * - field names take 2*k'th indices + * - corresponding variable names take (2*k + 1)'th indices. + * + * 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. + */ +@SinceKotlin("1.3") +internal fun BaseContinuationImpl.getSpilledVariableFieldMapping(): Array { + val debugMetadata = getDebugMetadataAnnotation() + val res = arrayListOf() + val label = getLabel() + for ((i, labelOfIndex) in debugMetadata.indexToLabel.withIndex()) { + if (labelOfIndex == label) { + res.add(debugMetadata.spilled[i]) + res.add(debugMetadata.localNames[i]) + } + } + return res.toTypedArray() +} \ No newline at end of file diff --git a/libraries/stdlib/coroutines/jvm/test/DebugMetadataTest.kt b/libraries/stdlib/coroutines/jvm/test/DebugMetadataTest.kt new file mode 100644 index 00000000000..87f783cc88f --- /dev/null +++ b/libraries/stdlib/coroutines/jvm/test/DebugMetadataTest.kt @@ -0,0 +1,63 @@ +/* + * 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. + */ + +@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "CANNOT_OVERRIDE_INVISIBLE_MEMBER") + +package test.kotlin.coroutines + +import org.junit.Test +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.jvm.internal.* +import kotlin.test.assertEquals + +/* + * 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. + */ + +@DebugMetadata( + sourceFiles = ["test.kt", "test1.kt", "test.kt"], + lineNumbers = [10, 2, 11], + indexToLabel = [0, 0, 1, 1, 2], + localNames = ["a", "b", "b", "c", "c"], + spilled = ["L$1", "L$2", "L$1", "L$2", "L$1"], + methodName = "testMethod", + className = "SomeClass" +) +private class MyContinuation : BaseContinuationImpl(null) { + override val context: CoroutineContext + get() = EmptyCoroutineContext + + var label = 0 + + override fun invokeSuspend(result: SuccessOrFailure): Any? = null +} + +class DebugMetadataTest { + @Test + fun testRuntimeDebugMetadata() { + val myContinuation = MyContinuation() + + myContinuation.label = 1 + assertEquals( + StackTraceElement("SomeClass", "testMethod", "test.kt", 10), + myContinuation.getStackTraceElement() + ) + assertEquals(listOf("L$1", "a", "L$2", "b"), myContinuation.getSpilledVariableFieldMapping().toList()) + myContinuation.label = 2 + assertEquals( + StackTraceElement("SomeClass", "testMethod", "test1.kt", 2), + myContinuation.getStackTraceElement() + ) + assertEquals(listOf("L$1", "b", "L$2", "c"), myContinuation.getSpilledVariableFieldMapping().toList()) + myContinuation.label = 3 + assertEquals( + StackTraceElement("SomeClass", "testMethod", "test.kt", 11), + myContinuation.getStackTraceElement() + ) + assertEquals(listOf("L$1", "c"), myContinuation.getSpilledVariableFieldMapping().toList()) + } +}