From 25222b55e610e4e24c0320e4b7219c84594a7bce Mon Sep 17 00:00:00 2001 From: Vladimir Ilmov Date: Mon, 18 Nov 2019 18:32:51 +0300 Subject: [PATCH] [coroutine][debug] AsyncStackTraceProvider refactored --- .../idea/debugger/AsyncStackTraceContext.kt | 152 ++++++++++++++++ ...KotlinCoroutinesAsyncStackTraceProvider.kt | 165 ++---------------- .../coroutines/CoroutinesDebuggerTree.kt | 3 +- .../debugger/coroutines/CoroutinesPanel.kt | 2 +- 4 files changed, 172 insertions(+), 150 deletions(-) create mode 100644 idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/AsyncStackTraceContext.kt diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/AsyncStackTraceContext.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/AsyncStackTraceContext.kt new file mode 100644 index 00000000000..d50a04091b5 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/AsyncStackTraceContext.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger + +import com.intellij.debugger.DebuggerContext +import com.intellij.debugger.engine.JavaValue +import com.intellij.debugger.engine.evaluation.EvaluateException +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.debugger.jdi.GeneratedLocation +import com.intellij.debugger.memory.utils.StackFrameItem +import com.intellij.debugger.ui.impl.watch.ValueDescriptorImpl +import com.intellij.xdebugger.frame.XNamedValue +import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext +import com.sun.jdi.* +import org.jetbrains.kotlin.codegen.coroutines.CONTINUATION_VARIABLE_NAME + +class AsyncStackTraceContext( + val context: ExecutionContext, + val method: Method, + private val debugMetadataKtType: ClassType +) { + + fun getAsyncStackTraceForSuspendLambda() : List? { + if (method.name() != "invokeSuspend" || method.signature() != "(Ljava/lang/Object;)Ljava/lang/Object;") { + return null + } + + val thisObject = context.frameProxy.thisObject() ?: return null + val thisType = thisObject.referenceType() + + if (SUSPEND_LAMBDA_CLASSES.none { thisType.isSubtype(it) }) { + return null + } + + return collectFrames(thisObject) + } + + fun getAsyncStackTraceForSuspendFunction(): List? { + if ("Lkotlin/coroutines/Continuation;)" !in method.signature()) { + return null + } + + val frameProxy = context.frameProxy + val continuationVariable = frameProxy.safeVisibleVariableByName(CONTINUATION_VARIABLE_NAME) ?: return null + val continuation = frameProxy.getValue(continuationVariable) as? ObjectReference ?: return null + context.keepReference(continuation) + + return collectFrames(continuation) + } + + private fun collectFrames(continuation: ObjectReference): List? { + val frames = mutableListOf() + collectFramesRecursively(continuation, frames) + return frames + } + + private fun collectFramesRecursively(continuation: ObjectReference, consumer: MutableList) { + val continuationType = continuation.referenceType() as? ClassType ?: return + val baseContinuationSupertype = findBaseContinuationSuperSupertype(continuationType) ?: return + + val location = getLocation(continuation) + val spilledVariables = getSpilledVariables(continuation) ?: emptyList() + + if (location != null) { + consumer += StackFrameItem(location, spilledVariables) + } + + val completionField = baseContinuationSupertype.fieldByName("completion") ?: return + val completion = continuation.getValue(completionField) as? ObjectReference ?: return + collectFramesRecursively(completion, consumer) + } + + private fun getLocation(continuation: ObjectReference): Location? { + val getStackTraceElementMethod = debugMetadataKtType.methodsByName( + "getStackTraceElement", + "(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)Ljava/lang/StackTraceElement;" + ).firstOrNull() ?: return null + + val args = listOf(continuation) + + val stackTraceElement = context.invokeMethod(debugMetadataKtType, getStackTraceElementMethod, args) as? ObjectReference + ?: return null + + context.keepReference(stackTraceElement) + + val stackTraceElementType = stackTraceElement.referenceType().takeIf { it.name() == StackTraceElement::class.java.name } + ?: return null + + fun getValue(name: String, desc: String): Value? { + val method = stackTraceElementType.methodsByName(name, desc).single() + return context.invokeMethod(stackTraceElement, method, emptyList()) + } + + val className = (getValue("getClassName", "()Ljava/lang/String;") as? StringReference)?.value() ?: return null + val methodName = (getValue("getMethodName", "()Ljava/lang/String;") as? StringReference)?.value() ?: return null + val lineNumber = (getValue("getLineNumber", "()I") as? IntegerValue)?.value()?.takeIf { it >= 0 } ?: return null + + val locationClass = context.findClassSafe(className) ?: return null + return GeneratedLocation(context.debugProcess, locationClass, methodName, lineNumber) + } + + fun getSpilledVariables(continuation: ObjectReference): List? { + val getSpilledVariableFieldMappingMethod = debugMetadataKtType.methodsByName( + "getSpilledVariableFieldMapping", + "(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)[Ljava/lang/String;" + ).firstOrNull() ?: return null + + val args = listOf(continuation) + + val rawSpilledVariables = context.invokeMethod(debugMetadataKtType, getSpilledVariableFieldMappingMethod, args) as? ArrayReference + ?: return null + + context.keepReference(rawSpilledVariables) + + val length = rawSpilledVariables.length() / 2 + val spilledVariables = ArrayList(length) + + for (index in 0 until length) { + val fieldName = (rawSpilledVariables.getValue(2 * index) as? StringReference)?.value() ?: continue + val variableName = (rawSpilledVariables.getValue(2 * index + 1) as? StringReference)?.value() ?: continue + val field = continuation.referenceType().fieldByName(fieldName) ?: continue + + val valueDescriptor = object : ValueDescriptorImpl(context.project) { + override fun calcValueName() = variableName + override fun calcValue(evaluationContext: EvaluationContextImpl?) = continuation.getValue(field) + override fun getDescriptorEvaluation(context: DebuggerContext?) = + throw EvaluateException("Spilled variable evaluation is not supported") + } + + spilledVariables += JavaValue.create( + null, + valueDescriptor, + context.evaluationContext, + context.debugProcess.xdebugProcess!!.nodeManager, + false + ) + } + + return spilledVariables + } + + private tailrec fun findBaseContinuationSuperSupertype(type: ClassType): ClassType? { + if (type.name() == "kotlin.coroutines.jvm.internal.BaseContinuationImpl") { + return type + } + return findBaseContinuationSuperSupertype(type.superclass() ?: return null) + } +} + diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinCoroutinesAsyncStackTraceProvider.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinCoroutinesAsyncStackTraceProvider.kt index f42a2c76ee8..b70a1ccf4f3 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinCoroutinesAsyncStackTraceProvider.kt +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinCoroutinesAsyncStackTraceProvider.kt @@ -5,35 +5,20 @@ package org.jetbrains.kotlin.idea.debugger -import com.intellij.debugger.DebuggerContext import com.intellij.debugger.engine.JavaStackFrame -import com.intellij.debugger.engine.JavaValue import com.intellij.debugger.engine.SuspendContextImpl -import com.intellij.debugger.engine.evaluation.EvaluateException import com.intellij.debugger.engine.evaluation.EvaluationContextImpl -import com.intellij.debugger.jdi.GeneratedLocation import com.intellij.debugger.jdi.StackFrameProxyImpl import com.intellij.debugger.memory.utils.StackFrameItem -import com.intellij.debugger.ui.impl.watch.ValueDescriptorImpl -import com.intellij.openapi.diagnostic.Logger -import com.intellij.xdebugger.frame.XNamedValue import com.sun.jdi.* -import org.jetbrains.kotlin.codegen.coroutines.CONTINUATION_VARIABLE_NAME import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext class KotlinCoroutinesAsyncStackTraceProvider : KotlinCoroutinesAsyncStackTraceProviderBase { private companion object { const val DEBUG_METADATA_KT = "kotlin.coroutines.jvm.internal.DebugMetadataKt" - - tailrec fun findBaseContinuationSuperSupertype(type: ClassType): ClassType? { - if (type.name() == "kotlin.coroutines.jvm.internal.BaseContinuationImpl") { - return type - } - - return findBaseContinuationSuperSupertype(type.superclass() ?: return null) - } } + override fun getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: SuspendContextImpl): List? { return hopelessAware { getAsyncStackTraceSafe(stackFrame.stackFrameProxy, suspendContext) } } @@ -51,146 +36,30 @@ class KotlinCoroutinesAsyncStackTraceProvider : KotlinCoroutinesAsyncStackTraceP return null } - val evaluationContext = EvaluationContextImpl(suspendContext, frameProxy) - val context = ExecutionContext(evaluationContext, frameProxy) + val context = createExecutionContext(suspendContext, frameProxy) // DebugMetadataKt not found, probably old kotlin-stdlib version - val debugMetadataKtType = context.findClassSafe(DEBUG_METADATA_KT) ?: return null + val debugMetadataKtType = findDebugMetadata(context) ?: return null val asyncContext = AsyncStackTraceContext(context, method, debugMetadataKtType) return asyncContext.getAsyncStackTraceForSuspendLambda() ?: asyncContext.getAsyncStackTraceForSuspendFunction() } - private fun AsyncStackTraceContext.getAsyncStackTraceForSuspendLambda(): List? { - if (method.name() != "invokeSuspend" || method.signature() != "(Ljava/lang/Object;)Ljava/lang/Object;") { - return null - } + private fun findDebugMetadata(context: ExecutionContext): ClassType? = context.findClassSafe(DEBUG_METADATA_KT) - val thisObject = context.frameProxy.thisObject() ?: return null - val thisType = thisObject.referenceType() - - if (SUSPEND_LAMBDA_CLASSES.none { thisType.isSubtype(it) }) { - return null - } - - return collectFrames(thisObject) + private fun createExecutionContext( + suspendContext: SuspendContextImpl, + frameProxy: StackFrameProxyImpl + ): ExecutionContext { + val evaluationContext = EvaluationContextImpl(suspendContext, frameProxy) + return ExecutionContext(evaluationContext, frameProxy) } +} - private fun AsyncStackTraceContext.getAsyncStackTraceForSuspendFunction(): List? { - if ("Lkotlin/coroutines/Continuation;)" !in method.signature()) { - return null - } - - val frameProxy = context.frameProxy - val continuationVariable = frameProxy.safeVisibleVariableByName(CONTINUATION_VARIABLE_NAME) ?: return null - val continuation = frameProxy.getValue(continuationVariable) as? ObjectReference ?: return null - context.keepReference(continuation) - - return collectFrames(continuation) +internal fun ExecutionContext.findClassSafe(className: String): ClassType? { + return try { + findClass(className) as? ClassType + } catch (e: Throwable) { + null } - - private fun AsyncStackTraceContext.collectFrames(continuation: ObjectReference): List? { - val frames = mutableListOf() - collectFramesRecursively(continuation, frames) - return frames - } - - private fun AsyncStackTraceContext.collectFramesRecursively(continuation: ObjectReference, consumer: MutableList) { - val continuationType = continuation.referenceType() as? ClassType ?: return - val baseContinuationSupertype = findBaseContinuationSuperSupertype(continuationType) ?: return - - val location = getLocation(continuation) - val spilledVariables = getSpilledVariables(continuation) ?: emptyList() - - if (location != null) { - consumer += StackFrameItem(location, spilledVariables) - } - - val completionField = baseContinuationSupertype.fieldByName("completion") ?: return - val completion = continuation.getValue(completionField) as? ObjectReference ?: return - collectFramesRecursively(completion, consumer) - } - - private fun AsyncStackTraceContext.getLocation(continuation: ObjectReference): Location? { - val getStackTraceElementMethod = debugMetadataKtType.methodsByName( - "getStackTraceElement", - "(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)Ljava/lang/StackTraceElement;" - ).firstOrNull() ?: return null - - val args = listOf(continuation) - - val stackTraceElement = context.invokeMethod(debugMetadataKtType, getStackTraceElementMethod, args) as? ObjectReference - ?: return null - - context.keepReference(stackTraceElement) - - val stackTraceElementType = stackTraceElement.referenceType().takeIf { it.name() == StackTraceElement::class.java.name } - ?: return null - - fun getValue(name: String, desc: String): Value? { - val method = stackTraceElementType.methodsByName(name, desc).single() - return context.invokeMethod(stackTraceElement, method, emptyList()) - } - - val className = (getValue("getClassName", "()Ljava/lang/String;") as? StringReference)?.value() ?: return null - val methodName = (getValue("getMethodName", "()Ljava/lang/String;") as? StringReference)?.value() ?: return null - val lineNumber = (getValue("getLineNumber", "()I") as? IntegerValue)?.value()?.takeIf { it >= 0 } ?: return null - - val locationClass = context.findClassSafe(className) ?: return null - return GeneratedLocation(context.debugProcess, locationClass, methodName, lineNumber) - } - - fun AsyncStackTraceContext.getSpilledVariables(continuation: ObjectReference): List? { - val getSpilledVariableFieldMappingMethod = debugMetadataKtType.methodsByName( - "getSpilledVariableFieldMapping", - "(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)[Ljava/lang/String;" - ).firstOrNull() ?: return null - - val args = listOf(continuation) - - val rawSpilledVariables = context.invokeMethod(debugMetadataKtType, getSpilledVariableFieldMappingMethod, args) as? ArrayReference - ?: return null - - context.keepReference(rawSpilledVariables) - - val length = rawSpilledVariables.length() / 2 - val spilledVariables = ArrayList(length) - - for (index in 0 until length) { - val fieldName = (rawSpilledVariables.getValue(2 * index) as? StringReference)?.value() ?: continue - val variableName = (rawSpilledVariables.getValue(2 * index + 1) as? StringReference)?.value() ?: continue - val field = continuation.referenceType().fieldByName(fieldName) ?: continue - - val valueDescriptor = object : ValueDescriptorImpl(context.project) { - override fun calcValueName() = variableName - override fun calcValue(evaluationContext: EvaluationContextImpl?) = continuation.getValue(field) - override fun getDescriptorEvaluation(context: DebuggerContext?) = - throw EvaluateException("Spilled variable evaluation is not supported") - } - - spilledVariables += JavaValue.create( - null, - valueDescriptor, - context.evaluationContext, - context.debugProcess.xdebugProcess!!.nodeManager, - false - ) - } - - return spilledVariables - } - - private fun ExecutionContext.findClassSafe(className: String): ClassType? { - return try { - findClass(className) as? ClassType - } catch (e: Throwable) { - null - } - } - - class AsyncStackTraceContext( - val context: ExecutionContext, - val method: Method, - val debugMetadataKtType: ClassType - ) -} \ No newline at end of file +} diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebuggerTree.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebuggerTree.kt index c7a1cccb389..38e64a5f2b4 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebuggerTree.kt +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebuggerTree.kt @@ -39,6 +39,7 @@ import com.intellij.xdebugger.impl.XDebuggerManagerImpl import com.intellij.xdebugger.impl.ui.DebuggerUIUtil import com.sun.jdi.ClassType import javaslang.control.Either +import org.jetbrains.kotlin.idea.debugger.AsyncStackTraceContext import org.jetbrains.kotlin.idea.debugger.KotlinCoroutinesAsyncStackTraceProvider import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext import java.awt.event.MouseEvent @@ -241,7 +242,7 @@ class CoroutinesDebuggerTree(project: Project) : DebuggerTree(project) { val debugMetadataKtType = execContext .findClass("kotlin.coroutines.jvm.internal.DebugMetadataKt") as ClassType val vars = with(KotlinCoroutinesAsyncStackTraceProvider()) { - KotlinCoroutinesAsyncStackTraceProvider.AsyncStackTraceContext( + AsyncStackTraceContext( execContext, aMethod, debugMetadataKtType diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesPanel.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesPanel.kt index ea65ca25474..bbc8c4e83ba 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesPanel.kt +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesPanel.kt @@ -31,7 +31,7 @@ import java.awt.event.KeyEvent import java.util.* /** - * Actually added into ui in [CoroutinesDebugConfigurationExtension.registerCoroutinesPanel] + * Actually added into ui in [CoroutineXDebuggerManagerListener.registerCoroutinesPanel] * Some methods are copied from [com.intellij.debugger.ui.impl.ThreadsPanel] */ class CoroutinesPanel(project: Project, stateManager: DebuggerStateManager) : DebuggerTreePanel(project, stateManager) {