[coroutine][debugger] refactoring being done

X-view approach added for review, thread groups added
This commit is contained in:
Vladimir Ilmov
2019-11-20 13:17:17 +03:00
committed by Vladimir Ilmov
parent 7f0437da68
commit 61c5ef61cc
43 changed files with 2302 additions and 1486 deletions
@@ -254,6 +254,13 @@ debugger.field.watchpoints.properties.panel.field.access.label=Field &access
debugger.field.watchpoints.properties.panel.field.modification.label=Field &modification
debugger.field.watchpoints.properties.panel.field.initialization.label=Field &initialization
# Coroutine Debugger
debugger.session.tab.coroutine.title=Coroutines
debugger.session.tab.coroutine.message.failure=Failed to retrieve coroutine status
debugger.session.tab.coroutine.message.resume=Application resumed
debugger.session.tab.coroutine.message.error=Coroutine dump failed, see log
debugger.session.tab.xcoroutine.title=XCoroutines
# Android Lint
android.klint.inspections.group.name=Android Lint for Kotlin
@@ -28,7 +28,7 @@ class KotlinGradleCoroutineDebugProjectResolver : AbstractProjectResolverExtensi
val lines = arrayOf(
"gradle.taskGraph.beforeTask { Task task ->",
" if (task instanceof Test) {",
" def kotlinxCoroutinesDebugJar = task.classpath.find { it.name.contains(\"kotlinx-coroutines-debug\") }",
" def kotlinxCoroutinesDebugJar = task.classpath.find { it.name.startsWith(\"kotlinx-coroutines-debug\") }",
" if (kotlinxCoroutinesDebugJar)",
" task.jvmArgs (\"-javaagent:\${kotlinxCoroutinesDebugJar?.absolutePath}\", \"-ea\")",
" }",
@@ -38,5 +38,6 @@ class KotlinGradleCoroutineDebugProjectResolver : AbstractProjectResolverExtensi
initScriptConsumer.consume(script)
}
// supposed to be the same as [CoroutineProjectConnectionListener.kt].coroutineDebuggerEnabled
private fun coroutineDebuggerEnabled() = Registry.`is`("kotlin.debugger.coroutines")
}
@@ -1,152 +0,0 @@
/*
* 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 com.sun.jdi.*
import org.jetbrains.kotlin.codegen.coroutines.CONTINUATION_VARIABLE_NAME
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
class AsyncStackTraceContext(
val context: ExecutionContext,
val method: Method,
private val debugMetadataKtType: ClassType
) {
fun getAsyncStackTraceForSuspendLambda(): List<StackFrameItem>? {
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<StackFrameItem>? {
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<StackFrameItem>? {
val frames = mutableListOf<StackFrameItem>()
collectFramesRecursively(continuation, frames)
return frames
}
private fun collectFramesRecursively(continuation: ObjectReference, consumer: MutableList<StackFrameItem>) {
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<XNamedValue>? {
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<XNamedValue>(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)
}
}
@@ -1,65 +0,0 @@
/*
* Copyright 2010-2018 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.engine.JavaStackFrame
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.memory.utils.StackFrameItem
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
class KotlinCoroutinesAsyncStackTraceProvider : KotlinCoroutinesAsyncStackTraceProviderBase {
private companion object {
const val DEBUG_METADATA_KT = "kotlin.coroutines.jvm.internal.DebugMetadataKt"
}
override fun getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: SuspendContextImpl): List<StackFrameItem>? {
return hopelessAware { getAsyncStackTraceSafe(stackFrame.stackFrameProxy, suspendContext) }
}
fun getAsyncStackTraceSafe(frameProxy: StackFrameProxyImpl, suspendContext: SuspendContextImpl): List<StackFrameItem>? {
val location = frameProxy.location()
if (!location.isInKotlinSources()) {
return null
}
val method = location.safeMethod() ?: return null
val threadReference = frameProxy.threadProxy().threadReference
if (threadReference == null || !threadReference.isSuspended || !suspendContext.debugProcess.canRunEvaluation) {
return null
}
val context = createExecutionContext(suspendContext, frameProxy)
// DebugMetadataKt not found, probably old kotlin-stdlib version
val debugMetadataKtType = findDebugMetadata(context) ?: return null
val asyncContext = AsyncStackTraceContext(context, method, debugMetadataKtType)
return asyncContext.getAsyncStackTraceForSuspendLambda() ?: asyncContext.getAsyncStackTraceForSuspendFunction()
}
private fun findDebugMetadata(context: ExecutionContext): ClassType? = context.findClassSafe(DEBUG_METADATA_KT)
private fun createExecutionContext(
suspendContext: SuspendContextImpl,
frameProxy: StackFrameProxyImpl
): ExecutionContext {
val evaluationContext = EvaluationContextImpl(suspendContext, frameProxy)
return ExecutionContext(evaluationContext, frameProxy)
}
}
internal fun ExecutionContext.findClassSafe(className: String): ClassType? {
return try {
findClass(className) as? ClassType
} catch (e: Throwable) {
null
}
}
@@ -1,15 +0,0 @@
/*
* 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.engine.AsyncStackTraceProvider
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.memory.utils.StackFrameItem
interface KotlinCoroutinesAsyncStackTraceProviderBase : AsyncStackTraceProvider {
override fun getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: SuspendContextImpl): List<StackFrameItem>?
}
@@ -0,0 +1,146 @@
/*
* 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.coroutines
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.ui.impl.watch.ValueDescriptorImpl
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
import org.jetbrains.kotlin.idea.debugger.coroutines.util.logger
import org.jetbrains.kotlin.idea.debugger.SUSPEND_LAMBDA_CLASSES
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineAsyncStackFrameItem
import org.jetbrains.kotlin.idea.debugger.isSubtype
import org.jetbrains.kotlin.idea.debugger.safeVisibleVariableByName
class AsyncStackTraceContext(
val context: ExecutionContext,
val method: Method) {
val log by logger
val debugMetadataKtType = context.findClassSafe(
DEBUG_METADATA_KT
)!!
private companion object {
const val DEBUG_METADATA_KT = "kotlin.coroutines.jvm.internal.DebugMetadataKt"
}
fun getAsyncStackTraceIfAny() : List<CoroutineAsyncStackFrameItem> {
val continuation = locateContinuation() ?: return emptyList()
val frames = mutableListOf<CoroutineAsyncStackFrameItem>()
collectFramesRecursively(continuation, frames)
return frames;
}
private fun locateContinuation() : ObjectReference? {
val continuation : ObjectReference?
if (isInvokeSuspendMethod(method)) {
continuation = context.frameProxy.thisObject() ?: return null
if (!isSuspendLambda(continuation.referenceType()))
return null
} else if (isContinuationProvider(method)) {
val frameProxy = context.frameProxy
val continuationVariable = frameProxy.safeVisibleVariableByName(CONTINUATION_VARIABLE_NAME) ?: return null
continuation = frameProxy.getValue(continuationVariable) as? ObjectReference ?: return null
context.keepReference(continuation)
} else {
continuation = null
}
return continuation
}
private fun isInvokeSuspendMethod(method: Method): Boolean =
method.name() == "invokeSuspend" && method.signature() == "(Ljava/lang/Object;)Ljava/lang/Object;"
private fun isContinuationProvider(method: Method): Boolean =
"Lkotlin/coroutines/Continuation;)" in method.signature()
private fun isSuspendLambda(referenceType: ReferenceType): Boolean =
SUSPEND_LAMBDA_CLASSES.any { referenceType.isSubtype(it) }
private fun collectFramesRecursively(continuation: ObjectReference, consumer: MutableList<CoroutineAsyncStackFrameItem>) {
val continuationType = continuation.referenceType() as? ClassType ?: return
val baseContinuationSupertype = findBaseContinuationSuperSupertype(continuationType) ?: return
val location = createLocation(continuation)
val spilledVariables = getSpilledVariables(continuation) ?: emptyList()
location?.let {
consumer.add(CoroutineAsyncStackFrameItem(location, spilledVariables))
}
val completionField = baseContinuationSupertype.fieldByName("completion") ?: return
val completion = continuation.getValue(completionField) as? ObjectReference ?: return
collectFramesRecursively(completion, consumer)
}
private fun createLocation(continuation: ObjectReference) : GeneratedLocation? {
val instance = invokeGetStackTraceElement(continuation) ?: return null
val className = context.invokeMethodAsString(instance, "getClassName") ?: return null
val methodName = context.invokeMethodAsString(instance, "getMethodName") ?: return null
val lineNumber = context.invokeMethodAsInt(instance,"getLineNumber") ?: return null
val locationClass = context.findClassSafe(className) ?: return null
log.warn("Got location of ${className}.${methodName}:${lineNumber} in ${locationClass}")
return GeneratedLocation(context.debugProcess, locationClass, methodName, lineNumber)
}
private fun invokeGetStackTraceElement(continuation: ObjectReference): ObjectReference? {
val stackTraceElement =
context.invokeMethodAsObject(debugMetadataKtType, "getStackTraceElement", continuation) ?: return null
// redundant i believe
stackTraceElement.referenceType().takeIf { it.name() == StackTraceElement::class.java.name } ?: return null
context.keepReference(stackTraceElement)
return stackTraceElement
}
fun getSpilledVariables(continuation: ObjectReference): List<XNamedValue>? {
val rawSpilledVariables =
context.invokeMethodAsArray(debugMetadataKtType, "getSpilledVariableFieldMapping", "(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)[Ljava/lang/String;", continuation) as? ArrayReference ?: return null
context.keepReference(rawSpilledVariables)
val length = rawSpilledVariables.length() / 2
val spilledVariables = ArrayList<XNamedValue>(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)
}
}
@@ -0,0 +1,53 @@
/*
* 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.coroutines
import com.intellij.debugger.engine.*
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.memory.utils.StackFrameItem
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.*
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineAsyncStackFrameItem
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
class CoroutineAsyncStackTraceProvider : AsyncStackTraceProvider {
override fun getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: SuspendContextImpl): List<CoroutineAsyncStackFrameItem>? {
val stackFrameList = hopelessAware { getAsyncStackTraceSafe(stackFrame.stackFrameProxy, suspendContext) } ?: emptyList()
return null
}
fun getAsyncStackTraceSafe(frameProxy: StackFrameProxyImpl, suspendContext: SuspendContext): List<CoroutineAsyncStackFrameItem> {
val defaultResult = emptyList<CoroutineAsyncStackFrameItem>()
val location = frameProxy.location()
if (!location.isInKotlinSources())
return defaultResult
val method = location.safeMethod() ?: return defaultResult
val threadReference = frameProxy.threadProxy().threadReference
if (threadReference == null || !threadReference.isSuspended || !(suspendContext.debugProcess as DebugProcessImpl).canRunEvaluation)
return defaultResult
val astContext = createAsyncStackTraceContext(frameProxy, suspendContext, method)
return astContext.getAsyncStackTraceIfAny()
}
private fun createAsyncStackTraceContext(
frameProxy: StackFrameProxyImpl,
suspendContext: SuspendContext,
method: Method
): AsyncStackTraceContext {
val evaluationContext = EvaluationContextImpl(suspendContext as SuspendContextImpl, frameProxy)
val context = ExecutionContext(evaluationContext, frameProxy)
// DebugMetadataKt not found, probably old kotlin-stdlib version
return AsyncStackTraceContext(context, method)
}
}
@@ -0,0 +1,23 @@
/*
* 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.coroutines
import org.jetbrains.annotations.NonNls
class CoroutineDebuggerContentInfo {
companion object {
val COROUTINE_THREADS_CONTENT = "CoroutineThreadsContent"
val XCOROUTINE_THREADS_CONTENT = "XCoroutineThreadsContent"
val XCOROUTINE_POPUP_ACTION_GROUP = "Kotlin.XDebugger.Actions"
}
}
class CoroutineDebuggerActions {
companion object {
@NonNls
val COROUTINE_PANEL_POPUP: String = "Debugger.CoroutinesPanelPopup"
}
}
@@ -0,0 +1,170 @@
/*
* 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.coroutines
import com.intellij.debugger.DebuggerInvocationUtil
import com.intellij.debugger.actions.ThreadDumpAction
import com.intellij.debugger.engine.JavaDebugProcess
import com.intellij.debugger.impl.DebuggerSession
import com.intellij.execution.configurations.JavaParameters
import com.intellij.execution.configurations.RunConfigurationBase
import com.intellij.execution.configurations.RunnerSettings
import com.intellij.execution.ui.RunnerLayoutUi
import com.intellij.execution.ui.layout.PlaceInGrid
import com.intellij.execution.ui.layout.impl.RunnerContentUi
import com.intellij.execution.ui.layout.impl.RunnerLayoutUiImpl
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
import com.intellij.ui.OnePixelSplitter
import com.intellij.ui.ScrollPaneFactory
import com.intellij.ui.content.Content
import com.intellij.ui.content.ContentManagerAdapter
import com.intellij.ui.content.ContentManagerEvent
import com.intellij.util.messages.MessageBusConnection
import com.intellij.xdebugger.*
import com.intellij.xdebugger.impl.XDebugSessionImpl
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.debugger.coroutines.util.*
import org.jetbrains.kotlin.idea.debugger.coroutines.view.CoroutinesPanel
import org.jetbrains.kotlin.idea.debugger.coroutines.view.XCoroutineView
import java.util.concurrent.atomic.AtomicInteger
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
class CoroutineProjectConnectionListener(val project: Project) : XDebuggerManagerListener {
var connection: MessageBusConnection? = null
val processCounter = AtomicInteger(0)
private val log by logger
private fun connect() {
connection = project.messageBus.connect()
connection?.subscribe(XDebuggerManager.TOPIC, this)
}
fun configurationStarting(
configuration: RunConfigurationBase<*>,
params: JavaParameters?,
runnerSettings: RunnerSettings?
) {
val configurationName = configuration.type.id
try {
if (!gradleConfiguration(configurationName)) { // gradle test logic in KotlinGradleCoroutineDebugProjectResolver
val kotlinxCoroutinesClassPathLib =
params?.classPath?.pathList?.first { it.contains("kotlinx-coroutines-debug") }
initializeCoroutineAgent(params!!, kotlinxCoroutinesClassPathLib)
}
starting()
} catch (e: NoSuchElementException) {
log.warn("'kotlinx-coroutines-debug' not found in classpath. Coroutine debugger disabled.")
}
}
private fun starting() {
if (processCounter.compareAndSet(0, 1))
connect()
else
processCounter.incrementAndGet()
}
private fun gradleConfiguration(configurationName: String) =
"GradleRunConfiguration".equals(configurationName) || "KotlinGradleRunConfiguration".equals(configurationName)
override fun processStarted(debugProcess: XDebugProcess) =
DebuggerInvocationUtil.swingInvokeLater(project) {
if (debugProcess is JavaDebugProcess)
registerCoroutinesPanel(debugProcess.session, debugProcess.debuggerSession)
}
override fun processStopped(debugProcess: XDebugProcess) {
if (processCounter.compareAndSet(1, 0)) {
connection?.disconnect()
connection = null
} else
processCounter.decrementAndGet()
}
private fun createThreadsContent(session: XDebugSession) {
val ui = session.ui ?: return
val xCoroutineThreadView = XCoroutineView(project, session as XDebugSessionImpl)
val framesContent: Content = createContent(ui, xCoroutineThreadView)
framesContent.isCloseable = false
ui.addContent(framesContent, 0, PlaceInGrid.right, false)
session.addSessionListener(xCoroutineThreadView.debugSessionListener(session))
session.rebuildViews()
}
private fun createContent(ui: RunnerLayoutUi, createContentParamProvider: CreateContentParamsProvider): Content {
val param = createContentParamProvider.createContentParams()
return ui.createContent(param.id, param.component, param.displayName, param.icon, param.parentComponent)
}
/**
* Adds panel to XDebugSessionTab
*/
private fun registerCoroutinesPanel(session: XDebugSession, debuggerSession: DebuggerSession): Boolean {
val ui = session.ui ?: return false
val panel = CoroutinesPanel(project, debuggerSession.contextManager)
// evaluation of `debuggerSession.contextManager.toString()` leads to
// java.lang.Throwable: Assertion failed: Should be invoked in manager thread, use DebuggerManagerThreadImpl.getInstance(..).invoke
// as toString() is not allowed here
createThreadsContent(session)
val content = ui.createContent(
CoroutineDebuggerContentInfo.COROUTINE_THREADS_CONTENT,
panel,
KotlinBundle.message("debugger.session.tab.coroutine.title"),
AllIcons.Debugger.ThreadGroup,
panel)
content.isCloseable = false
ui.addContent(content, 0, PlaceInGrid.left, true)
ui.addListener(object : ContentManagerAdapter() {
override fun selectionChanged(event: ContentManagerEvent) {
if (event.content === content) {
if (content.isSelected) {
panel.setUpdateEnabled(true)
if (panel.isRefreshNeeded) {
panel.rebuildIfVisible(DebuggerSession.Event.CONTEXT)
}
} else {
panel.setUpdateEnabled(false)
}
}
}
}, content)
// add coroutine dump button: due to api problem left toolbar is copied, modified and reset to tab
val runnerContent = (ui.options as RunnerLayoutUiImpl).getData(RunnerContentUi.KEY.name) as RunnerContentUi
val modifiedActions = runnerContent.getActions(true)
val pos = modifiedActions.indexOfLast { it is ThreadDumpAction }
modifiedActions.add(pos + 1, ActionManager.getInstance().getAction("Kotlin.XDebugger.CoroutinesDump"))
ui.options.setLeftToolbar(DefaultActionGroup(modifiedActions), ActionPlaces.DEBUGGER_TOOLBAR)
return true
}
}
val Project.coroutineConnectionListener by projectListener
val projectListener
get() = object : ReadOnlyProperty<Project, CoroutineProjectConnectionListener> {
lateinit var listenerProject: CoroutineProjectConnectionListener
override fun getValue(project: Project, property: KProperty<*>): CoroutineProjectConnectionListener {
if (!::listenerProject.isInitialized)
listenerProject = CoroutineProjectConnectionListener(project)
return listenerProject
}
}
internal fun coroutineDebuggerEnabled() = Registry.`is`("kotlin.debugger.coroutines")
internal fun initializeCoroutineAgent(params: JavaParameters, it: String?) {
params.vmParametersList?.add("-javaagent:$it")
params.vmParametersList?.add("-ea")
}
@@ -1,105 +0,0 @@
/*
* 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.coroutines
import com.intellij.debugger.DebuggerInvocationUtil
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.actions.ThreadDumpAction
import com.intellij.debugger.impl.DebuggerSession
import com.intellij.execution.configurations.JavaParameters
import com.intellij.execution.configurations.RunConfigurationBase
import com.intellij.execution.ui.RunnerLayoutUi
import com.intellij.execution.ui.layout.PlaceInGrid
import com.intellij.execution.ui.layout.impl.RunnerContentUi
import com.intellij.execution.ui.layout.impl.RunnerLayoutUiImpl
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.registry.Registry
import com.intellij.ui.content.ContentManagerAdapter
import com.intellij.ui.content.ContentManagerEvent
import com.intellij.util.messages.MessageBusConnection
import com.intellij.xdebugger.XDebugProcess
import com.intellij.xdebugger.XDebuggerManager
import com.intellij.xdebugger.XDebuggerManagerListener
import org.jetbrains.kotlin.psi.UserDataProperty
class CoroutineXDebuggerManagerListener(val project: Project) : XDebuggerManagerListener {
lateinit var connection: MessageBusConnection
override fun processStarted(debugProcess: XDebugProcess) {
DebuggerInvocationUtil.swingInvokeLater(project) {
val session = DebuggerManagerEx.getInstanceEx(project).context.debuggerSession
val ui = session?.xDebugSession?.ui
if (ui != null)
registerCoroutinesPanel(ui, session)
}
}
override fun processStopped(debugProcess: XDebugProcess) {
connection.disconnect()
project.listenerCreated = false
}
/**
* Adds panel to XDebugSessionTab
*/
private fun registerCoroutinesPanel(ui: RunnerLayoutUi, session: DebuggerSession) {
val panel = CoroutinesPanel(session.project, session.contextManager)
val content = ui.createContent(
"CoroutinesContent", panel, "Coroutines", // TODO(design)
AllIcons.Debugger.ThreadGroup, null
)
content.isCloseable = false
ui.addContent(content, 0, PlaceInGrid.left, true)
ui.addListener(object : ContentManagerAdapter() {
override fun selectionChanged(event: ContentManagerEvent) {
if (event.content === content) {
if (content.isSelected) {
panel.setUpdateEnabled(true)
if (panel.isRefreshNeeded) {
panel.rebuildIfVisible(DebuggerSession.Event.CONTEXT)
}
} else {
panel.setUpdateEnabled(false)
}
}
}
}, content)
// add coroutine dump button: due to api problem left toolbar is copied, modified and reset to tab
val runnerContent = (ui.options as RunnerLayoutUiImpl).getData(RunnerContentUi.KEY.name) as RunnerContentUi
val modifiedActions = runnerContent.getActions(true)
val pos = modifiedActions.indexOfLast { it is ThreadDumpAction }
modifiedActions.add(pos + 1, ActionManager.getInstance().getAction("Kotlin.XDebugger.CoroutinesDump"))
ui.options.setLeftToolbar(DefaultActionGroup(modifiedActions), ActionPlaces.DEBUGGER_TOOLBAR)
}
fun attach() {
connection = project.messageBus.connect()
connection.subscribe(XDebuggerManager.TOPIC, this)
project.listenerCreated = true
}
}
internal var Project.listenerCreated: Boolean? by UserDataProperty(Key.create("COROUTINES_DEBUG_TAB_CREATE_LISTENER"))
internal fun isCoroutineDebuggerEnabled() = Registry.`is`("kotlin.debugger.coroutines")
internal fun initializeCoroutineAgent(params: JavaParameters, it: String?) {
params.vmParametersList?.add("-javaagent:$it")
params.vmParametersList?.add("-ea")
}
internal fun <T : RunConfigurationBase<*>?> registerProjectCoroutineListener(configuration: T) {
val project = (configuration as RunConfigurationBase<*>).project
// add listener to put coroutines tab into debugger tab
if (project.listenerCreated != true) { // prevent multiple listeners creation
CoroutineXDebuggerManagerListener(project).attach()
}
}
@@ -9,31 +9,22 @@ import com.intellij.execution.configurations.DebuggingRunnerData
import com.intellij.execution.configurations.JavaParameters
import com.intellij.execution.configurations.RunConfigurationBase
import com.intellij.execution.configurations.RunnerSettings
import com.intellij.openapi.diagnostic.Logger
/**
* Installs coroutines debug agent and coroutines tab if `kotlinx.coroutines.debug` dependency is found
* Installs coroutines debug agent and coroutines tab if `kotlinx-coroutines-debug` dependency is found
*/
@Suppress("IncompatibleAPI")
class CoroutinesDebugConfigurationExtension : RunConfigurationExtension() {
private val log = Logger.getInstance(this::class.java)
override fun isApplicableFor(configuration: RunConfigurationBase<*>) = isCoroutineDebuggerEnabled()
override fun isApplicableFor(configuration: RunConfigurationBase<*>) = coroutineDebuggerEnabled()
override fun <T : RunConfigurationBase<*>?> updateJavaParameters(
configuration: T,
params: JavaParameters?,
runnerSettings: RunnerSettings?
) {
if (!isCoroutineDebuggerEnabled()) return
if (runnerSettings is DebuggingRunnerData) {
try {
val kotlinxCoroutinesClassPathLib = params?.classPath?.pathList?.first { it.contains("kotlinx-coroutines-debug") }
initializeCoroutineAgent(params!!, kotlinxCoroutinesClassPathLib)
registerProjectCoroutineListener(configuration)
} catch (e: NoSuchElementException) {
log.warn("'kotlinx-coroutines-debug' not found in classpath. Coroutine debugger disabled.")
}
if (runnerSettings is DebuggingRunnerData && configuration is RunConfigurationBase<*>) {
configuration.project.coroutineConnectionListener.configurationStarting(configuration, params, runnerSettings)
}
}
}
}
@@ -9,31 +9,22 @@ import com.intellij.execution.configurations.DebuggingRunnerData
import com.intellij.execution.configurations.JavaParameters
import com.intellij.execution.configurations.RunConfigurationBase
import com.intellij.execution.configurations.RunnerSettings
import com.intellij.openapi.diagnostic.Logger
/**
* Installs coroutines debug agent and coroutines tab if `kotlinx.coroutines.debug` dependency is found
* Installs coroutines debug agent and coroutines tab if `kotlinx-coroutines-debug` dependency is found
*/
@Suppress("IncompatibleAPI")
class CoroutinesDebugConfigurationExtension : RunConfigurationExtension() {
private val log = Logger.getInstance(this::class.java)
override fun isApplicableFor(configuration: RunConfigurationBase<*>) = isCoroutineDebuggerEnabled()
override fun isApplicableFor(configuration: RunConfigurationBase<*>) = coroutineDebuggerEnabled()
override fun <T : RunConfigurationBase<*>?> updateJavaParameters(
configuration: T,
params: JavaParameters,
runnerSettings: RunnerSettings?
) {
if (!isCoroutineDebuggerEnabled()) return
if (runnerSettings is DebuggingRunnerData) {
try {
val kotlinxCoroutinesClassPathLib = params?.classPath?.pathList?.first { it.contains("kotlinx-coroutines-debug") }
initializeCoroutineAgent(params!!, kotlinxCoroutinesClassPathLib)
registerProjectCoroutineListener(configuration)
} catch (e: NoSuchElementException) {
log.warn("'kotlinx-coroutines-debug' not found in classpath. Coroutine debugger disabled.")
}
if (runnerSettings is DebuggingRunnerData && configuration is RunConfigurationBase<*>) {
configuration.project.coroutineConnectionListener.configurationStarting(configuration, params, runnerSettings)
}
}
}
}
@@ -1,193 +0,0 @@
/*
* 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.coroutines
import com.intellij.debugger.engine.DebugProcess
import com.intellij.openapi.util.Key
import com.sun.jdi.*
import javaslang.control.Either
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
import org.jetbrains.kotlin.psi.UserDataProperty
object CoroutinesDebugProbesProxy {
private const val DEBUG_PACKAGE = "kotlinx.coroutines.debug"
private var DebugProcess.references by UserDataProperty(Key.create<ProcessReferences>("COROUTINES_DEBUG_REFERENCES"))
@Synchronized
@Suppress("unused")
fun install(context: ExecutionContext) {
val debugProbes = context.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType
val instance = with(debugProbes) { getValue(fieldByName("INSTANCE")) as ObjectReference }
val install = debugProbes.concreteMethodByName("install", "()V")
context.invokeMethod(instance, install, emptyList())
}
@Synchronized
@Suppress("unused")
fun uninstall(context: ExecutionContext) {
val debugProbes = context.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType
val instance = with(debugProbes) { getValue(fieldByName("INSTANCE")) as ObjectReference }
val uninstall = debugProbes.concreteMethodByName("uninstall", "()V")
context.invokeMethod(instance, uninstall, emptyList())
}
/**
* Invokes DebugProbes from debugged process's classpath and returns states of coroutines
* Should be invoked on debugger manager thread
*/
@Synchronized
fun dumpCoroutines(context: ExecutionContext): Either<Throwable, List<CoroutineState>> {
try {
var refs = context.debugProcess.references
if (refs == null) {
refs = ProcessReferences(context)
context.debugProcess.references = refs
}
// get dump
val infoList = context.invokeMethod(refs.instance, refs.dumpMethod, emptyList()) as? ObjectReference
?: return Either.right(emptyList())
context.keepReference(infoList)
val size = (context.invokeMethod(infoList, refs.getSize, emptyList()) as IntegerValue).value()
return Either.right(List(size) {
val index = context.vm.mirrorOf(it)
// `List<CoroutineInfo>.get(index)`
val elem = context.invokeMethod(infoList, refs.getElement, listOf(index)) as ObjectReference
val name = getName(context, elem, refs)
val state = getState(context, elem, refs)
val thread = getLastObservedThread(elem, refs.threadRef)
CoroutineState(
name,
CoroutineState.State.valueOf(state),
thread,
getStackTrace(elem, refs, context),
elem.getValue(refs.continuation) as? ObjectReference
)
})
} catch (e: Throwable) {
return Either.left(e)
}
}
private fun getName(
context: ExecutionContext, // Execution context to invoke methods
info: ObjectReference, // CoroutineInfo instance
refs: ProcessReferences
): String {
// equals to `coroutineInfo.context.get(CoroutineName).name`
val coroutineContextInst = context.invokeMethod(
info,
refs.getContext,
emptyList()
) as? ObjectReference ?: throw IllegalArgumentException("Coroutine context must not be null")
val coroutineName = context.invokeMethod(
coroutineContextInst,
refs.getContextElement, listOf(refs.nameKey)
) as? ObjectReference
// If the coroutine doesn't have a given name, CoroutineContext.get(CoroutineName) returns null
val name = if (coroutineName != null) (context.invokeMethod(
coroutineName,
refs.getName,
emptyList()
) as StringReference).value() else "coroutine"
val id = (info.getValue(refs.idField) as LongValue).value()
return "$name#$id"
}
private fun getState(
context: ExecutionContext, // Execution context to invoke methods
info: ObjectReference, // CoroutineInfo instance
refs: ProcessReferences
): String {
// equals to `stringState = coroutineInfo.state.toString()`
val state = context.invokeMethod(info, refs.getState, emptyList()) as ObjectReference
return (context.invokeMethod(state, refs.toString, emptyList()) as StringReference).value()
}
private fun getLastObservedThread(
info: ObjectReference, // CoroutineInfo instance
threadRef: Field // reference to lastObservedThread
): ThreadReference? = info.getValue(threadRef) as? ThreadReference
/**
* Returns list of stackTraceElements for the given CoroutineInfo's [ObjectReference]
*/
private fun getStackTrace(
info: ObjectReference,
refs: ProcessReferences,
context: ExecutionContext
): List<StackTraceElement> {
val frameList = context.invokeMethod(info, refs.lastObservedStackTrace, emptyList()) as ObjectReference
val mergedFrameList = context.invokeMethod(
refs.debugProbesImpl,
refs.enhanceStackTraceWithThreadDump, listOf(info, frameList)
) as ObjectReference
val size = (context.invokeMethod(mergedFrameList, refs.getSize, emptyList()) as IntegerValue).value()
val list = ArrayList<StackTraceElement>()
for (it in size - 1 downTo 0) {
val frame = context.invokeMethod(
mergedFrameList, refs.getElement,
listOf(context.vm.virtualMachine.mirrorOf(it))
) as ObjectReference
val clazz = (frame.getValue(refs.className) as? StringReference)?.value()
list.add(
0, // add in the beginning
StackTraceElement(
clazz,
(frame.getValue(refs.methodName) as? StringReference)?.value(),
(frame.getValue(refs.fileName) as? StringReference)?.value(),
(frame.getValue(refs.line) as IntegerValue).value()
)
)
}
return list
}
/**
* Holds ClassTypes, Methods, ObjectReferences and Fields for a particular jvm
*/
private class ProcessReferences(context: ExecutionContext) {
// kotlinx.coroutines.debug.DebugProbes instance and methods
val debugProbes = context.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType
val probesImplType = context.findClass("$DEBUG_PACKAGE.internal.DebugProbesImpl") as ClassType
val debugProbesImpl = with(probesImplType) { getValue(fieldByName("INSTANCE")) as ObjectReference }
val enhanceStackTraceWithThreadDump: Method = probesImplType
.methodsByName("enhanceStackTraceWithThreadDump").single()
val dumpMethod: Method = debugProbes.concreteMethodByName("dumpCoroutinesInfo", "()Ljava/util/List;")
val instance = with(debugProbes) { getValue(fieldByName("INSTANCE")) as ObjectReference }
// CoroutineInfo
val info = context.findClass("$DEBUG_PACKAGE.CoroutineInfo") as ClassType
val getState: Method = info.concreteMethodByName("getState", "()Lkotlinx/coroutines/debug/State;")
val getContext: Method = info.concreteMethodByName("getContext", "()Lkotlin/coroutines/CoroutineContext;")
val idField: Field = info.fieldByName("sequenceNumber")
val lastObservedStackTrace: Method = info.methodsByName("lastObservedStackTrace").single()
val coroutineContext = context.findClass("kotlin.coroutines.CoroutineContext") as InterfaceType
val getContextElement: Method = coroutineContext.methodsByName("get").single()
val coroutineName = context.findClass("kotlinx.coroutines.CoroutineName") as ClassType
val getName: Method = coroutineName.methodsByName("getName").single()
val nameKey = coroutineName.getValue(coroutineName.fieldByName("Key")) as ObjectReference
val toString: Method = (context.findClass("java.lang.Object") as ClassType)
.concreteMethodByName("toString", "()Ljava/lang/String;")
val threadRef: Field = info.fieldByName("lastObservedThread")
val continuation: Field = info.fieldByName("lastObservedFrame")
// Methods for list
val listType = context.findClass("java.util.List") as InterfaceType
val getSize: Method = listType.methodsByName("size").single()
val getElement: Method = listType.methodsByName("get").single()
val element = context.findClass("java.lang.StackTraceElement") as ClassType
// for StackTraceElement
val methodName: Field = element.fieldByName("methodName")
val className: Field = element.fieldByName("declaringClass")
val fileName: Field = element.fieldByName("fileName")
val line: Field = element.fieldByName("lineNumber")
}
}
@@ -1,451 +0,0 @@
/*
* 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.coroutines
import com.intellij.debugger.DebuggerBundle
import com.intellij.debugger.DebuggerInvocationUtil
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.actions.GotoFrameSourceAction
import com.intellij.debugger.engine.*
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.engine.events.DebuggerCommandImpl
import com.intellij.debugger.engine.events.SuspendContextCommandImpl
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.impl.DebuggerSession
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl
import com.intellij.debugger.memory.utils.StackFrameItem
import com.intellij.debugger.ui.impl.tree.TreeBuilder
import com.intellij.debugger.ui.impl.tree.TreeBuilderNode
import com.intellij.debugger.ui.impl.watch.*
import com.intellij.debugger.ui.tree.StackFrameDescriptor
import com.intellij.ide.DataManager
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.MessageType
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.ui.DoubleClickListener
import com.intellij.xdebugger.XDebuggerUtil
import com.intellij.xdebugger.XSourcePosition
import com.intellij.xdebugger.frame.XExecutionStack
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
import java.lang.ref.WeakReference
import javax.swing.event.TreeModelEvent
import javax.swing.event.TreeModelListener
/**
* Tree of coroutines for [CoroutinesPanel]
*/
class CoroutinesDebuggerTree(project: Project) : DebuggerTree(project) {
private val logger = Logger.getInstance(this::class.java)
private var lastSuspendContextCache: Cache? = null
override fun createNodeManager(project: Project): NodeManagerImpl {
return object : NodeManagerImpl(project, this) {
override fun getContextKey(frame: StackFrameProxyImpl?): String? {
return "CoroutinesView"
}
}
}
/**
* Prepare specific behavior instead of DebuggerTree constructor
*/
init {
val model = object : TreeBuilder(this) {
override fun buildChildren(node: TreeBuilderNode) {
val debuggerTreeNode = node as DebuggerTreeNodeImpl
if (debuggerTreeNode.descriptor is DefaultNodeDescriptor) {
return
}
node.add(myNodeManager.createMessageNode(MessageDescriptor.EVALUATING))
buildNode(debuggerTreeNode)
}
override fun isExpandable(builderNode: TreeBuilderNode): Boolean {
return this@CoroutinesDebuggerTree.isExpandable(builderNode as DebuggerTreeNodeImpl)
}
}
model.setRoot(nodeFactory.defaultNode)
model.addTreeModelListener(createListener())
setModel(model)
emptyText.text = "Coroutines are not available"
}
/**
* Add frames inside coroutine (node)
*/
private fun buildNode(node: DebuggerTreeNodeImpl) {
val context = DebuggerManagerEx.getInstanceEx(project).context
val debugProcess = context.debugProcess
debugProcess?.managerThread?.schedule(object : SuspendContextCommandImpl(context.suspendContext) {
override fun contextAction(suspendContext: SuspendContextImpl) {
val evalContext = debuggerContext.createEvaluationContext() ?: return
if (node.descriptor is CoroutineDescriptorImpl || node.descriptor is CreationFramesDescriptor) {
val children = mutableListOf<DebuggerTreeNodeImpl>()
try {
addChildren(children, debugProcess, node.descriptor, evalContext)
} catch (e: EvaluateException) {
children.clear()
children.add(myNodeManager.createMessageNode(e.message))
logger.debug(e)
}
DebuggerInvocationUtil.swingInvokeLater(project) {
node.removeAllChildren()
for (debuggerTreeNode in children) {
node.add(debuggerTreeNode)
}
node.childrenChanged(true)
}
}
}
})
}
fun installAction(): () -> Unit {
val listener = object : DoubleClickListener() {
override fun onDoubleClick(e: MouseEvent): Boolean {
val location = getPathForLocation(e.x, e.y)
?.lastPathComponent as? DebuggerTreeNodeImpl ?: return false
return selectFrame(location.userObject)
}
}
listener.installOn(this)
return { listener.uninstall(this) }
}
fun selectFrame(descriptor: Any): Boolean {
val dataContext = DataManager.getInstance().getDataContext(this@CoroutinesDebuggerTree)
val context = DebuggerManagerEx.getInstanceEx(project).context
when (descriptor) {
is SuspendStackFrameDescriptor -> {
buildSuspendStackFrameChildren(descriptor)
return true
}
is AsyncStackFrameDescriptor -> {
buildAsyncStackFrameChildren(descriptor, context.debugProcess ?: return false)
return true
}
is EmptyStackFrameDescriptor -> {
buildEmptyStackFrameChildren(descriptor)
return true
}
is StackFrameDescriptor -> {
GotoFrameSourceAction.doAction(dataContext)
return true
}
else -> return true
}
}
private fun buildSuspendStackFrameChildren(descriptor: SuspendStackFrameDescriptor) {
val context = DebuggerManagerEx.getInstanceEx(project).context
val pos = getPosition(descriptor.frame) ?: return
context.debugProcess?.managerThread?.schedule(object : SuspendContextCommandImpl(context.suspendContext) {
override fun contextAction() {
val (stack, stackFrame) = createSyntheticStackFrame(descriptor, pos) ?: return
val action: () -> Unit = { context.debuggerSession?.xDebugSession?.setCurrentStackFrame(stack, stackFrame) }
ApplicationManager.getApplication()
.invokeLater(action, ModalityState.stateForComponent(this@CoroutinesDebuggerTree))
}
})
}
private fun buildAsyncStackFrameChildren(descriptor: AsyncStackFrameDescriptor, process: DebugProcessImpl) {
process.managerThread?.schedule(object : DebuggerCommandImpl() {
override fun action() {
val context = DebuggerManagerEx.getInstanceEx(project).context
val proxy = ThreadReferenceProxyImpl(
process.virtualMachineProxy,
descriptor.state.thread // is not null because it's a running coroutine
)
val executionStack = JavaExecutionStack(proxy, process, false)
executionStack.initTopFrame()
val frame = descriptor.frame.createFrame(process)
DebuggerUIUtil.invokeLater {
context.debuggerSession?.xDebugSession?.setCurrentStackFrame(
executionStack,
frame
)
}
}
})
}
private fun buildEmptyStackFrameChildren(descriptor: EmptyStackFrameDescriptor) {
val position = getPosition(descriptor.frame) ?: return
val context = DebuggerManagerEx.getInstanceEx(project).context
val suspendContext = context.suspendContext ?: return
val proxy = suspendContext.thread ?: return
context.debugProcess?.managerThread?.schedule(object : DebuggerCommandImpl() {
override fun action() {
val executionStack =
JavaExecutionStack(proxy, context.debugProcess!!, false)
executionStack.initTopFrame()
val frame = SyntheticStackFrame(descriptor, emptyList(), position)
val action: () -> Unit =
{ context.debuggerSession?.xDebugSession?.setCurrentStackFrame(executionStack, frame) }
ApplicationManager.getApplication()
.invokeLater(action, ModalityState.stateForComponent(this@CoroutinesDebuggerTree))
}
})
}
private fun getPosition(frame: StackTraceElement): XSourcePosition? {
val psiFacade = JavaPsiFacade.getInstance(project)
val psiClass = psiFacade.findClass(
frame.className.substringBefore("$"), // find outer class, for which psi exists TODO
GlobalSearchScope.everythingScope(project)
)
val classFile = psiClass?.containingFile?.virtualFile
// to convert to 0-based line number or '-1' to do not move
val lineNumber = if(frame.lineNumber > 0) frame.lineNumber - 1 else return null
return XDebuggerUtil.getInstance().createPosition(classFile, lineNumber)
}
/**
* Should be invoked on manager thread
*/
private fun createSyntheticStackFrame(
descriptor: SuspendStackFrameDescriptor,
pos: XSourcePosition
): Pair<XExecutionStack, SyntheticStackFrame>? {
val context = DebuggerManagerEx.getInstanceEx(project).context
val suspendContext = context.suspendContext ?: return null
val proxy = suspendContext.thread ?: return null
val executionStack = JavaExecutionStack(proxy, suspendContext.debugProcess, false)
executionStack.initTopFrame()
val evalContext = context.createEvaluationContext()
val frameProxy = evalContext?.frameProxy ?: return null
val execContext = ExecutionContext(evalContext, frameProxy)
val continuation = descriptor.continuation // guaranteed that it is a BaseContinuationImpl
val aMethod = (continuation.type() as ClassType).concreteMethodByName(
"getStackTraceElement",
"()Ljava/lang/StackTraceElement;"
)
val debugMetadataKtType = execContext
.findClass("kotlin.coroutines.jvm.internal.DebugMetadataKt") as ClassType
val vars = with(KotlinCoroutinesAsyncStackTraceProvider()) {
AsyncStackTraceContext(
execContext,
aMethod,
debugMetadataKtType
).getSpilledVariables(continuation)
} ?: return null
return executionStack to SyntheticStackFrame(descriptor, vars, pos)
}
private fun addChildren(
children: MutableList<DebuggerTreeNodeImpl>,
debugProcess: DebugProcessImpl,
descriptor: NodeDescriptorImpl,
evalContext: EvaluationContextImpl
) {
val creationStackTraceSeparator = "\b\b\b" // the "\b\b\b" is used for creation stacktrace separator in kotlinx.coroutines
if (descriptor !is CoroutineDescriptorImpl) {
if (descriptor is CreationFramesDescriptor) {
val threadProxy = debuggerContext.suspendContext?.thread ?: return
val proxy = threadProxy.forceFrames().first()
descriptor.frames.forEach {
children.add(myNodeManager.createNode(EmptyStackFrameDescriptor(it, proxy), evalContext))
}
}
return
}
when (descriptor.state.state) {
CoroutineState.State.RUNNING -> {
if (descriptor.state.thread == null) {
children.add(myNodeManager.createMessageNode("Frames are not available"))
return
}
val proxy = ThreadReferenceProxyImpl(
debugProcess.virtualMachineProxy,
descriptor.state.thread
)
val frames = proxy.forceFrames()
var i = frames.lastIndex
while (i > 0 && frames[i].location().method().name() != "resumeWith") i--
// if i is less than 0, wait, what?
for (frame in 0..--i) {
children.add(createFrameDescriptor(descriptor, evalContext, frames[frame]))
}
if (i > 0) { // add async stack trace if there are frames after invokeSuspend
val async = KotlinCoroutinesAsyncStackTraceProvider().getAsyncStackTrace(
JavaStackFrame(StackFrameDescriptorImpl(frames[i - 1], MethodsTracker()), true),
evalContext.suspendContext
)
async?.forEach { children.add(createAsyncFrameDescriptor(descriptor, evalContext, it, frames[0])) }
}
for (frame in i + 2..frames.lastIndex) {
children.add(createFrameDescriptor(descriptor, evalContext, frames[frame]))
}
}
CoroutineState.State.SUSPENDED -> {
val threadProxy = debuggerContext.suspendContext?.thread ?: return
val proxy = threadProxy.forceFrames().first()
// the thread is paused on breakpoint - it has at least one frame
for (it in descriptor.state.stackTrace) {
if (it.className.startsWith(creationStackTraceSeparator)) break
children.add(createCoroutineFrameDescriptor(descriptor, evalContext, it, proxy))
}
}
else -> {
}
}
val trace = descriptor.state.stackTrace
val index = trace.indexOfFirst { it.className.startsWith(creationStackTraceSeparator) }
children.add(myNodeManager.createNode(CreationFramesDescriptor(trace.subList(index + 1, trace.size)), evalContext))
}
private fun createFrameDescriptor(
descriptor: NodeDescriptorImpl,
evalContext: EvaluationContextImpl,
frame: StackFrameProxyImpl
): DebuggerTreeNodeImpl {
return myNodeManager.createNode(
myNodeManager.getStackFrameDescriptor(descriptor, frame),
evalContext
)
}
private fun createCoroutineFrameDescriptor(
descriptor: CoroutineDescriptorImpl,
evalContext: EvaluationContextImpl,
frame: StackTraceElement,
proxy: StackFrameProxyImpl,
parent: NodeDescriptorImpl? = null
): DebuggerTreeNodeImpl {
return myNodeManager.createNode(
myNodeManager.getDescriptor(
parent,
CoroutineStackFrameData(descriptor.state, frame, proxy)
), evalContext
)
}
private fun createAsyncFrameDescriptor(
descriptor: CoroutineDescriptorImpl,
evalContext: EvaluationContextImpl,
frame: StackFrameItem,
proxy: StackFrameProxyImpl
): DebuggerTreeNodeImpl {
return myNodeManager.createNode(
myNodeManager.getDescriptor(
descriptor,
CoroutineStackFrameData(descriptor.state, frame, proxy)
), evalContext
)
}
private fun createListener() = object : TreeModelListener {
override fun treeNodesChanged(event: TreeModelEvent) {
hideTooltip()
}
override fun treeNodesInserted(event: TreeModelEvent) {
hideTooltip()
}
override fun treeNodesRemoved(event: TreeModelEvent) {
hideTooltip()
}
override fun treeStructureChanged(event: TreeModelEvent) {
hideTooltip()
}
}
override fun isExpandable(node: DebuggerTreeNodeImpl): Boolean {
val descriptor = node.descriptor
return if (descriptor is StackFrameDescriptor) false else descriptor.isExpandable
}
override fun build(context: DebuggerContextImpl) {
val session = context.debuggerSession
val command = RefreshCoroutinesTreeCommand(session, context.suspendContext)
val state = if (session != null) session.state else DebuggerSession.State.DISPOSED
if (ApplicationManager.getApplication().isUnitTestMode
|| state == DebuggerSession.State.PAUSED
) {
showMessage(MessageDescriptor.EVALUATING)
context.debugProcess!!.managerThread.schedule(command)
} else {
showMessage(if (session != null) session.stateDescription else DebuggerBundle.message("status.debug.stopped"))
}
}
private inner class RefreshCoroutinesTreeCommand(private val mySession: DebuggerSession?, context: SuspendContextImpl?) :
SuspendContextCommandImpl(context) {
override fun contextAction() {
val root = nodeFactory.defaultNode
mySession ?: return
val suspendContext = suspendContext
if (suspendContext == null || suspendContext.isResumed) {
setRoot(root.apply { add(myNodeManager.createMessageNode("Application is resumed")) })
return
}
val evaluationContext = EvaluationContextImpl(suspendContext, suspendContext.frameProxy)
val executionContext = ExecutionContext(evaluationContext, suspendContext.frameProxy ?: return)
val cache = lastSuspendContextCache
val states = if (cache != null && cache.first.get() === suspendContext) {
cache.second
} else CoroutinesDebugProbesProxy.dumpCoroutines(executionContext).apply {
lastSuspendContextCache = WeakReference(suspendContext) to this
}
// if suspend context hasn't changed - use last dump, else compute new
if (states.isLeft) {
logger.warn(states.left)
setRoot(root.apply {
clear()
add(nodeFactory.createMessageNode(MessageDescriptor("Dump failed")))
})
XDebuggerManagerImpl.NOTIFICATION_GROUP
.createNotification(
"Coroutine dump failed. See log",
MessageType.ERROR
).notify(project)
return
}
for (state in states.get()) {
root.add(
nodeFactory.createNode(
nodeFactory.getDescriptor(null, CoroutineData(state)), evaluationContext
)
)
}
setRoot(root)
}
private fun setRoot(root: DebuggerTreeNodeImpl) {
DebuggerInvocationUtil.swingInvokeLater(project) {
mutableModel.setRoot(root)
treeChanged()
}
}
}
}
private typealias Cache = Pair<WeakReference<SuspendContextImpl>, Either<Throwable, List<CoroutineState>>>
@@ -1,162 +0,0 @@
/*
* 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.
*/
@file:Suppress("DEPRECATION")
package org.jetbrains.kotlin.idea.debugger.coroutines
import com.intellij.debugger.engine.events.SuspendContextCommandImpl
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.impl.DebuggerContextListener
import com.intellij.debugger.impl.DebuggerSession
import com.intellij.debugger.impl.DebuggerStateManager
import com.intellij.debugger.ui.impl.DebuggerTreePanel
import com.intellij.debugger.ui.impl.watch.DebuggerTree
import com.intellij.debugger.ui.impl.watch.DebuggerTreeNodeImpl
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPopupMenu
import com.intellij.openapi.actionSystem.EmptyActionGroup
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.ui.ScrollPaneFactory
import com.intellij.util.Alarm
import org.jetbrains.annotations.NonNls
import java.awt.BorderLayout
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.util.*
/**
* 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) {
private val myUpdateLabelsAlarm = Alarm(Alarm.ThreadToUse.SWING_THREAD)
init {
val disposable = getCoroutinesTree().installAction()
registerDisposable(disposable)
getCoroutinesTree().addKeyListener(object : KeyAdapter() {
override fun keyPressed(e: KeyEvent) {
if (e.keyCode == KeyEvent.VK_ENTER && getCoroutinesTree().selectionCount == 1) {
val selected = getCoroutinesTree().selectionModel.selectionPath.lastPathComponent
if (selected is DebuggerTreeNodeImpl) getCoroutinesTree().selectFrame(selected.userObject)
}
}
})
add(ScrollPaneFactory.createScrollPane(getCoroutinesTree()), BorderLayout.CENTER)
stateManager.addListener(object : DebuggerContextListener {
override fun changeEvent(newContext: DebuggerContextImpl, event: DebuggerSession.Event) {
if (DebuggerSession.Event.ATTACHED == event || DebuggerSession.Event.RESUME == event) {
startLabelsUpdate()
} else if (DebuggerSession.Event.PAUSE == event
|| DebuggerSession.Event.DETACHED == event
|| DebuggerSession.Event.DISPOSE == event
) {
myUpdateLabelsAlarm.cancelAllRequests()
}
if (DebuggerSession.Event.DETACHED == event || DebuggerSession.Event.DISPOSE == event) {
stateManager.removeListener(this)
}
}
})
startLabelsUpdate()
}
private fun startLabelsUpdate() {
if (myUpdateLabelsAlarm.isDisposed) return
myUpdateLabelsAlarm.cancelAllRequests()
myUpdateLabelsAlarm.addRequest(object : Runnable {
override fun run() {
var updateScheduled = false
try {
if (isUpdateEnabled) {
val tree = getCoroutinesTree()
val root = tree.model.root as DebuggerTreeNodeImpl
val process = context.debugProcess
if (process != null) {
process.managerThread.schedule(object : SuspendContextCommandImpl(context.suspendContext) {
override fun contextAction() {
try {
updateNodeLabels(root)
} finally {
reschedule()
}
}
override fun commandCancelled() {
reschedule()
}
})
updateScheduled = true
}
}
} finally {
if (!updateScheduled) {
reschedule()
}
}
}
private fun reschedule() {
val session = context.debuggerSession
if (session != null && session.isAttached && !session.isPaused && !myUpdateLabelsAlarm.isDisposed) {
myUpdateLabelsAlarm.addRequest(
this,
LABELS_UPDATE_DELAY_MS, ModalityState.NON_MODAL
)
}
}
}, LABELS_UPDATE_DELAY_MS, ModalityState.NON_MODAL)
}
override fun dispose() {
Disposer.dispose(myUpdateLabelsAlarm)
super.dispose()
}
// copied from com.intellij.debugger.ui.impl.ThreadsPanel.updateNodeLabels
private fun updateNodeLabels(from: DebuggerTreeNodeImpl) {
val children = from.children()
try {
while (children.hasMoreElements()) {
val child = children.nextElement() as DebuggerTreeNodeImpl
child.descriptor.updateRepresentation(
null
) { child.labelChanged() }
updateNodeLabels(child)
}
} catch (ignored: NoSuchElementException) { // children have changed - just skip
}
}
override fun createTreeView(): DebuggerTree {
return CoroutinesDebuggerTree(project)
}
override fun createPopupMenu(): ActionPopupMenu {
val group = EmptyActionGroup()
return ActionManager.getInstance().createActionPopupMenu("Debugger.CoroutinesPanelPopup", group)
}
override fun getData(dataId: String): Any? {
return if (PlatformDataKeys.HELP_ID.`is`(dataId)) {
HELP_ID
} else super.getData(dataId)
}
fun getCoroutinesTree(): CoroutinesDebuggerTree = tree as CoroutinesDebuggerTree
companion object {
@NonNls
private val HELP_ID = "debugging.debugCoroutines"
private const val LABELS_UPDATE_DELAY_MS = 200
}
}
@@ -0,0 +1,36 @@
/*
* 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.coroutines.command
import com.intellij.debugger.DebuggerInvocationUtil
import com.intellij.debugger.engine.events.DebuggerContextCommandImpl
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.impl.PrioritizedTask
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl
import com.intellij.debugger.ui.impl.watch.DebuggerTreeNodeImpl
import com.intellij.debugger.ui.impl.watch.NodeManagerImpl
open class BuildCoroutineNodeCommand(
val node: DebuggerTreeNodeImpl,
debuggerContext: DebuggerContextImpl,
val myNodeManager: NodeManagerImpl,
thread: ThreadReferenceProxyImpl? = null
) : DebuggerContextCommandImpl(debuggerContext, thread) {
protected val myChildren = mutableListOf<DebuggerTreeNodeImpl>()
override fun getPriority() = PrioritizedTask.Priority.NORMAL
protected fun updateUI(scrollToVisible: Boolean) {
DebuggerInvocationUtil.swingInvokeLater(debuggerContext.project) {
node.removeAllChildren()
for (debuggerTreeNode in myChildren) {
node.add(debuggerTreeNode)
}
node.childrenChanged(scrollToVisible)
}
}
}
@@ -0,0 +1,31 @@
/*
* 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.coroutines.command
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.ui.impl.watch.DebuggerTreeNodeImpl
import com.intellij.debugger.ui.impl.watch.NodeManagerImpl
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CreationFramesDescriptor
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineCreatedStackFrameDescriptor
class CoroutineBuildCreationFrameCommand(
node: DebuggerTreeNodeImpl,
val descriptor: CreationFramesDescriptor,
nodeManager: NodeManagerImpl,
debuggerContext: DebuggerContextImpl
) : BuildCoroutineNodeCommand(node, debuggerContext, nodeManager) {
override fun threadAction() {
val threadProxy = debuggerContext.suspendContext?.thread ?: return
val evalContext = debuggerContext.createEvaluationContext() ?: return
val proxy = threadProxy.forceFrames().first()
descriptor.frames.forEach {
val descriptor = myNodeManager.createNode(
CoroutineCreatedStackFrameDescriptor(it, proxy), evalContext)
myChildren.add(descriptor)
}
updateUI(true)
}
}
@@ -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.coroutines.command
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.evaluation.EvaluationContext
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl
import com.intellij.debugger.memory.utils.StackFrameItem
import com.intellij.debugger.ui.impl.watch.*
import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineAsyncStackTraceProvider
import org.jetbrains.kotlin.idea.debugger.coroutines.data.*
@Deprecated("Moved to XCoroutineView")
class CoroutineBuildFrameCommand(
node: DebuggerTreeNodeImpl,
val descriptor: CoroutineDescriptorImpl,
nodeManager: NodeManagerImpl,
debuggerContext: DebuggerContextImpl
) : BuildCoroutineNodeCommand(node, debuggerContext, nodeManager) {
companion object {
val creationStackTraceSeparator = "\b\b\b" // the "\b\b\b" is used for creation stacktrace separator in kotlinx.coroutines
}
override fun threadAction() {
val debugProcess = debuggerContext.debugProcess ?: return
val evalContext = debuggerContext.createEvaluationContext() ?: return
when (descriptor.infoData.state) {
CoroutineInfoData.State.RUNNING -> {
if (renderRunningCoroutine(debugProcess, evalContext)) return
}
CoroutineInfoData.State.SUSPENDED, CoroutineInfoData.State.CREATED -> {
if (renderSuspendedCoroutine(evalContext)) return
}
}
createCreationStackTraceDescriptor(evalContext)
updateUI(true)
}
private fun createCreationStackTraceDescriptor(evalContext: EvaluationContextImpl) {
val threadProxy = debuggerContext.suspendContext?.thread ?: return
val proxy = threadProxy.forceFrames().first()
val trace = descriptor.infoData.stackTrace
val index = trace.indexOfFirst { it.className.startsWith(creationStackTraceSeparator) }
val creationNode = myNodeManager.createNode(
CreationFramesDescriptor(trace.subList(index + 1, trace.size)), evalContext)
myChildren.add(creationNode)
trace.subList(index + 1, trace.size).forEach {
val descriptor = myNodeManager.createNode(
CoroutineCreatedStackFrameDescriptor(it, proxy), evalContext)
creationNode.add(descriptor)
}
}
private fun renderSuspendedCoroutine(evalContext: EvaluationContextImpl): Boolean {
val threadProxy = debuggerContext.suspendContext?.thread ?: return true
val proxy = threadProxy.forceFrames().first()
// the thread is paused on breakpoint - it has at least one frame
for (it in descriptor.infoData.stackTrace) {
if (it.className.startsWith(creationStackTraceSeparator)) break
myChildren.add(createCoroutineFrameDescriptor(evalContext, it, proxy))
}
return false
}
private fun createCoroutineFrameDescriptor(
evalContext: EvaluationContextImpl,
frame: StackTraceElement,
proxy: StackFrameProxyImpl,
parent: NodeDescriptorImpl? = null
): DebuggerTreeNodeImpl {
return myNodeManager.createNode(
myNodeManager.getDescriptor(
parent,
CoroutineStackTraceData(descriptor.infoData, proxy, evalContext, frame)
), evalContext
)
}
private fun renderRunningCoroutine(
debugProcess: DebugProcessImpl,
evalContext: EvaluationContextImpl
): Boolean {
if (descriptor.infoData.thread == null) {
myChildren.add(myNodeManager.createMessageNode("Frames are not available"))
return true
}
val proxy = ThreadReferenceProxyImpl(
debugProcess.virtualMachineProxy,
descriptor.infoData.thread
)
val frames = proxy.forceFrames()
var endRange = findResumeWithMethodFrameIndex(frames)
for (frame in 0..frames.lastIndex) {
if (frame == endRange) {
val javaStackFrame = JavaStackFrame(StackFrameDescriptorImpl(frames[endRange - 1], MethodsTracker()), true)
val async = CoroutineAsyncStackTraceProvider()
.getAsyncStackTrace(javaStackFrame, evalContext.suspendContext)
async?.forEach {
myChildren.add(createAsyncFrameDescriptor(evalContext, it, frames[frame]))
}
} else {
val frameDescriptor = createFrameDescriptor(evalContext, frames[frame])
myChildren.add(frameDescriptor)
}
}
updateUI(true)
return false
}
private fun findResumeWithMethodFrameIndex(frames: List<StackFrameProxyImpl>) : Int {
for (j: Int in frames.lastIndex downTo 0 )
if (isResumeMethodFrame(frames[j])) {
return j
}
return 0
}
private fun isResumeMethodFrame(frame: StackFrameProxyImpl) = frame.location().method().name() == "resumeWith"
private fun createFrameDescriptor(
evalContext: EvaluationContext,
frame: StackFrameProxyImpl
): DebuggerTreeNodeImpl {
return myNodeManager.createNode(
myNodeManager.getStackFrameDescriptor(descriptor, frame),
evalContext
)
}
private fun createAsyncFrameDescriptor(
evalContext: EvaluationContextImpl,
frame: StackFrameItem,
proxy: StackFrameProxyImpl
): DebuggerTreeNodeImpl {
return myNodeManager.createNode(
myNodeManager.getDescriptor(
descriptor,
CoroutineStackFrameData(descriptor.infoData, proxy, evalContext, frame)
), evalContext
)
}
}
@@ -0,0 +1,240 @@
/*
* 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.coroutines.command
import com.intellij.debugger.engine.*
import com.intellij.debugger.jdi.ClassesByNameProvider
import com.intellij.debugger.jdi.GeneratedLocation
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl
import com.intellij.debugger.memory.utils.StackFrameItem
import com.intellij.debugger.ui.impl.watch.MethodsTracker
import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl
import com.intellij.util.containers.ContainerUtil
import com.intellij.xdebugger.XSourcePosition
import com.intellij.xdebugger.frame.XStackFrame
import com.sun.jdi.AbsentInformationException
import com.sun.jdi.Location
import com.sun.jdi.ReferenceType
import com.sun.jdi.ThreadReference
import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineAsyncStackTraceProvider
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData
import org.jetbrains.kotlin.idea.debugger.coroutines.util.getPosition
class CoroutineBuilder(val suspendContext: SuspendContextImpl) {
private val methodsTracker = MethodsTracker()
private val coroutineStackFrameProvider = CoroutineAsyncStackTraceProvider()
val virtualMachineProxy = suspendContext.debugProcess.virtualMachineProxy
val classesByName = ClassesByNameProvider.createCache(virtualMachineProxy.allClasses())
companion object {
val CREATION_STACK_TRACE_SEPARATOR = "\b\b\b" // the "\b\b\b" is used for creation stacktrace separator in kotlinx.coroutines
}
fun build(ci: CoroutineInfoData): List<CoroutineStackFrameItem> {
val coroutineStackFrameList = mutableListOf<CoroutineStackFrameItem>()
val suspendedThreadProxy = suspendedThreadProxy()
val firstSuspendedStackFrameProxyImpl = firstSuspendedThreadFrame()
val creationFrameSeparatorIndex = findCreationFrameIndex(ci.stackTrace)
val runningThreadReferenceProxyImpl = currentRunningThreadProxy(ci.thread ?: suspendedThreadProxy().threadReference)
val positionManager = suspendContext.debugProcess.positionManager
if (ci.state == CoroutineInfoData.State.RUNNING && runningThreadReferenceProxyImpl is ThreadReferenceProxyImpl) {
val executionStack = JavaExecutionStack(runningThreadReferenceProxyImpl, suspendContext.debugProcess, suspendContext.thread == runningThreadReferenceProxyImpl)
val frames = runningThreadReferenceProxyImpl.forceFrames()
var resumeMethodIndex = findResumeMethodIndex(frames)
for (frameIndex in 0..frames.lastIndex) {
val runningStackFrameProxy = frames[frameIndex]
if (frameIndex == resumeMethodIndex) {
val previousFrame = frames[resumeMethodIndex - 1]
val previousJavaFrame = JavaStackFrame(StackFrameDescriptorImpl(previousFrame, methodsTracker), true)
val asyncStackTrace = coroutineStackFrameProvider
.getAsyncStackTrace(previousJavaFrame, suspendContext)
asyncStackTrace?.forEach {
// val xStackFrame = positionManager.createStackFrame(runningStackFrameProxy, suspendContext.debugProcess, it.location)
val xStackFrame = JavaStackFrame(StackFrameDescriptorImpl(previousFrame, methodsTracker), true)
// val itStackFrame = null
coroutineStackFrameList.add(AsyncCoroutineStackFrameItem(runningStackFrameProxy, "some label", it, xStackFrame))
}
} else {
// val xStackFrame = stackFrame(positionManager, runningStackFrameProxy, runningStackFrameProxy.location())
val xStackFrame = executionStack.createStackFrame(runningStackFrameProxy)
coroutineStackFrameList.add(RunningCoroutineStackFrameItem(runningStackFrameProxy, "", xStackFrame))
}
}
} else if (ci.state == CoroutineInfoData.State
.SUSPENDED || runningThreadReferenceProxyImpl == null
) { // to get frames from CoroutineInfo anyway
// the thread is paused on breakpoint - it has at least one frame
ci.stackTrace.subList(0, creationFrameSeparatorIndex).forEach {
val xStackFrame = stackFrame(positionManager, firstSuspendedStackFrameProxyImpl, it)
// val xStackFrame = null
coroutineStackFrameList.add(SuspendCoroutineStackFrameItem(firstSuspendedStackFrameProxyImpl, "suspended frame", it, xStackFrame))
}
}
val executionStack = JavaExecutionStack(suspendedThreadProxy(), suspendContext.debugProcess, false)
val xStackFrame = executionStack.createStackFrame(firstSuspendedStackFrameProxyImpl)
ci.stackTrace.subList(creationFrameSeparatorIndex + 1, ci.stackTrace.size).forEach {
var location = createLocation(it)
coroutineStackFrameList.add(CreationCoroutineStackFrameItem(firstSuspendedStackFrameProxyImpl, "creation frame", it, xStackFrame, location))
}
ci.stackFrameList.addAll(coroutineStackFrameList)
return coroutineStackFrameList
}
fun createLocation(stackTraceElement: StackTraceElement): Location {
return findLocation(
ContainerUtil.getFirstItem(classesByName[stackTraceElement.className]),
stackTraceElement.methodName,
stackTraceElement.lineNumber
)
}
private fun findLocation(
type: ReferenceType?,
methodName: String,
line: Int
): Location {
if (type != null && line >= 0) {
try {
val location = type.locationsOfLine(DebugProcess.JAVA_STRATUM, null, line).stream()
.filter { l: Location -> l.method().name() == methodName }
.findFirst().orElse(null)
if (location != null) {
return location
}
} catch (ignored: AbsentInformationException) {
}
}
return GeneratedLocation(suspendContext.debugProcess, type, methodName, line)
}
fun stackFrame(positionManager: CompoundPositionManager, runningStackFrameProxy: StackFrameProxyImpl, location: Location): XStackFrame {
return positionManager.createStackFrame(runningStackFrameProxy, suspendContext.debugProcess, location)!!
}
fun stackFrame(positionManager: CompoundPositionManager, runningStackFrameProxy: StackFrameProxyImpl, stackTraceElement: StackTraceElement) : XStackFrame {
val location = createLocation(stackTraceElement)
// val sourcePosition = getPosition(stackTraceElement)
return positionManager.createStackFrame(runningStackFrameProxy, suspendContext.debugProcess, location)!!
}
/**
* Tries to find creation frame separator if any, returns last index if none found
*/
private fun findCreationFrameIndex(frames: List<StackTraceElement>): Int {
var index = frames.indexOfFirst { isCreationSeparatorFrame(it) }
return if (index < 0)
frames.lastIndex
else
index
}
private fun isCreationSeparatorFrame(it: StackTraceElement) =
it.className.startsWith(CREATION_STACK_TRACE_SEPARATOR)
private fun firstSuspendedThreadFrame(): StackFrameProxyImpl =
suspendedThreadProxy().forceFrames().first()
// retrieves currently suspended but active and executing corouting thread proxy
fun currentRunningThreadProxy(threadReference: ThreadReference?): ThreadReferenceProxyImpl? =
ThreadReferenceProxyImpl(suspendContext.debugProcess.virtualMachineProxy, threadReference)
// retrieves current suspended thread proxy
fun suspendedThreadProxy(): ThreadReferenceProxyImpl =
suspendContext.thread!! // @TODO hash replace !!
private fun findResumeMethodIndex(frames: List<StackFrameProxyImpl>): Int {
for (j: Int in frames.lastIndex downTo 0)
if (isResumeMethodFrame(frames[j])) {
return j
}
return 0
}
private fun isResumeMethodFrame(frame: StackFrameProxyImpl) = frame.location().method().name() == "resumeWith"
/**
* Should be invoked on manager thread
*/
/*
private fun createSyntheticStackFrame(
descriptor: SuspendStackFrameDescriptor,
pos: XSourcePosition,
project: Project
): Pair<XExecutionStack, SyntheticStackFrame>? {
val context = DebuggerManagerEx.getInstanceEx(project).context
val suspendContext = context.suspendContext ?: return null
val proxy = suspendContext.thread ?: return null
val executionStack = JavaExecutionStack(proxy, suspendContext.debugProcess, false)
executionStack.initTopFrame()
val evalContext = context.createEvaluationContext()
val frameProxy = evalContext?.frameProxy ?: return null
val execContext = ExecutionContext(evalContext, frameProxy)
val continuation = descriptor.continuation // guaranteed that it is a BaseContinuationImpl
val aMethod = (continuation.type() as ClassType).concreteMethodByName(
"getStackTraceElement",
"()Ljava/lang/StackTraceElement;"
)
val vars = with(CoroutineAsyncStackTraceProvider()) {
AsyncStackTraceContext(
execContext,
aMethod
).getSpilledVariables(continuation)
} ?: return null
return executionStack to SyntheticStackFrame(descriptor, vars, pos)
}
*/
}
class CreationCoroutineStackFrameItem(
frame: StackFrameProxyImpl,
label: String = "",
val stackTraceElement: StackTraceElement,
stackFrame: XStackFrame,
val location: Location
) : CoroutineStackFrameItem(frame, label, stackFrame) {
override fun location() = location
}
class SuspendCoroutineStackFrameItem(
frame: StackFrameProxyImpl,
label: String = "",
val stackTraceElement: StackTraceElement,
stackFrame: XStackFrame
) : CoroutineStackFrameItem(frame, label, stackFrame) {
override fun location() = frame.location()
}
class AsyncCoroutineStackFrameItem(
frame: StackFrameProxyImpl,
label: String = "",
val frameItem: StackFrameItem,
stackFrame: XStackFrame
) : CoroutineStackFrameItem(frame, label, stackFrame) {
override fun location() : Location = frame.location()
}
class RunningCoroutineStackFrameItem(
frame: StackFrameProxyImpl,
label: String = "",
stackFrame: XStackFrame
) : CoroutineStackFrameItem(frame, label, stackFrame) {
val location = frame.location() // it should be invoked in manager thread
override fun location() = location
}
abstract class CoroutineStackFrameItem(val frame: StackFrameProxyImpl, val label: String = "", val stackFrame: XStackFrame) {
fun sourcePosition() : XSourcePosition? = stackFrame.sourcePosition
abstract fun location(): Location
}
@@ -3,7 +3,7 @@
* 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.coroutines
package org.jetbrains.kotlin.idea.debugger.coroutines.command
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.engine.events.SuspendContextCommandImpl
@@ -21,11 +21,14 @@ import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.MessageType
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.registry.Registry
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.text.DateFormatUtil
import com.intellij.xdebugger.impl.XDebuggerManagerImpl
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.debugger.coroutines.view.CoroutineDumpPanel
import org.jetbrains.kotlin.idea.debugger.coroutines.coroutineDebuggerEnabled
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData
import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.CoroutinesDebugProbesProxy
@Suppress("ComponentNotRegistered")
class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate {
@@ -39,28 +42,23 @@ class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate {
val process = context.debugProcess ?: return
process.managerThread.schedule(object : SuspendContextCommandImpl(context.suspendContext) {
override fun contextAction() {
val evalContext = context.createEvaluationContext()
val frameProxy = evalContext?.frameProxy ?: return
val execContext = ExecutionContext(evalContext, frameProxy)
val states = CoroutinesDebugProbesProxy.dumpCoroutines(execContext)
if (states.isLeft) {
logger.warn(states.left)
XDebuggerManagerImpl.NOTIFICATION_GROUP
.createNotification(
"Coroutine dump failed. See log",
MessageType.WARNING
).notify(project)
return
val states = CoroutinesDebugProbesProxy(context.suspendContext!!).dumpCoroutines() ?: return
if (states.isOk()) {
XDebuggerManagerImpl.NOTIFICATION_GROUP.createNotification(
KotlinBundle.message("debugger.session.tab.coroutine.message.error"),
MessageType.ERROR
).notify(project)
} else {
val f = fun() {
addCoroutineDump(
project,
states.cache,
session.xDebugSession?.ui ?: return,
session.searchScope
)
}
ApplicationManager.getApplication().invokeLater(f, ModalityState.NON_MODAL)
}
val f = fun() {
addCoroutineDump(
project,
states.get(),
session.xDebugSession?.ui ?: return,
session.searchScope
)
}
ApplicationManager.getApplication().invokeLater(f, ModalityState.NON_MODAL)
}
})
}
@@ -69,7 +67,7 @@ class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate {
/**
* Analog of [DebuggerUtilsEx.addThreadDump].
*/
fun addCoroutineDump(project: Project, coroutines: List<CoroutineState>, ui: RunnerLayoutUi, searchScope: GlobalSearchScope) {
fun addCoroutineDump(project: Project, coroutines: List<CoroutineInfoData>, ui: RunnerLayoutUi, searchScope: GlobalSearchScope) {
val consoleBuilder = TextConsoleBuilderFactory.getInstance().createBuilder(project)
consoleBuilder.filters(ExceptionFilters.getFilters(searchScope))
val consoleView = consoleBuilder.console
@@ -102,7 +100,7 @@ class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate {
return
}
val debuggerSession = DebuggerManagerEx.getInstanceEx(project).context.debuggerSession
presentation.isEnabled = debuggerSession != null && debuggerSession.isAttached && isCoroutineDebuggerEnabled()
presentation.isEnabled = debuggerSession != null && debuggerSession.isAttached && coroutineDebuggerEnabled()
presentation.isVisible = presentation.isEnabled
}
}
@@ -0,0 +1,78 @@
/*
* 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.coroutines.command
import com.intellij.debugger.DebuggerInvocationUtil
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.engine.events.SuspendContextCommandImpl
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.impl.DebuggerSession
import com.intellij.debugger.ui.impl.watch.DebuggerTreeNodeImpl
import com.intellij.debugger.ui.impl.watch.NodeManagerImpl
import com.intellij.openapi.ui.MessageType
import com.intellij.xdebugger.impl.XDebuggerManagerImpl
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineDescriptorData
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData
import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.CoroutinesDebugProbesProxy
import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.ManagerThreadExecutor
import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.createEvaluationContext
import org.jetbrains.kotlin.idea.debugger.coroutines.view.CoroutinesDebuggerTree
class RefreshCoroutinesTreeCommand(
val context: DebuggerContextImpl,
private val debuggerTree: CoroutinesDebuggerTree
) : SuspendContextCommandImpl(context.suspendContext) {
override fun contextAction() {
val nf = debuggerTree.nodeFactory
val root = nf.defaultNode
val sc: SuspendContextImpl? = suspendContext
if (context.debuggerSession is DebuggerSession && sc is SuspendContextImpl && !sc.isResumed) {
val infoCache = CoroutinesDebugProbesProxy(sc).dumpCoroutines()
if (infoCache.isOk()) {
val evaluationContext = sc.createEvaluationContext()
for (state in infoCache.cache) {
val descriptor = createCoroutineDescriptorNode(nf, state, evaluationContext)
root.add(descriptor)
}
setRoot(root)
} else {
debuggerTree.showMessage(KotlinBundle.message("debugger.session.tab.coroutine.message.failure"))
XDebuggerManagerImpl.NOTIFICATION_GROUP.createNotification(
KotlinBundle.message("debugger.session.tab.coroutine.message.error"),
MessageType.ERROR
)
.notify(debuggerTree.project)
}
} else {
debuggerTree.showMessage(KotlinBundle.message("debugger.session.tab.coroutine.message.resume"))
}
}
private fun createCoroutineDescriptorNode(
nodeFactory: NodeManagerImpl,
coroutineInfoData: CoroutineInfoData,
evaluationContext: EvaluationContextImpl
) =
nodeFactory.createNode(
nodeFactory.getDescriptor(
null,
CoroutineDescriptorData(coroutineInfoData)
),
evaluationContext
)
private fun setRoot(root: DebuggerTreeNodeImpl) {
DebuggerInvocationUtil.swingInvokeLater(debuggerTree.project) {
debuggerTree.mutableModel.setRoot(root)
debuggerTree.treeChanged()
}
}
}
@@ -0,0 +1,17 @@
/*
* 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.coroutines.data
import com.intellij.debugger.jdi.GeneratedLocation
import com.intellij.debugger.memory.utils.StackFrameItem
import com.intellij.xdebugger.frame.XNamedValue
class CoroutineAsyncStackFrameItem(
val location: GeneratedLocation,
spilledVariables: List<XNamedValue>
) : StackFrameItem(location, spilledVariables) {
}
@@ -0,0 +1,49 @@
/*
* 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.coroutines.data
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.coroutines.command.CoroutineStackFrameItem
/**
* Represents state of a coroutine.
* @see `kotlinx.coroutines.debug.CoroutineInfo`
*/
data class CoroutineInfoData(
val name: String,
val state: State,
val threadName: String,
val threadStatus: Int,
val stackTrace: List<StackTraceElement>,
// links to jdi.* references
val thread: ThreadReference? = null, // for suspended coroutines should be null
val frame: ObjectReference?
) {
var stackFrameList = mutableListOf<CoroutineStackFrameItem>()
// @TODO for refactoring/removal
val stringStackTrace: String by lazy {
buildString {
appendln("\"$name\", state: $state")
stackTrace.forEach {
appendln("\t$it")
}
}
}
fun isSuspended() = state == State.SUSPENDED
fun isCreated() = state == State.CREATED
fun isEmptyStackTrace() = stackTrace.isEmpty()
enum class State {
RUNNING,
SUSPENDED,
CREATED
}
}
@@ -3,7 +3,7 @@
* 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.coroutines
package org.jetbrains.kotlin.idea.debugger.coroutines.data
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl
@@ -0,0 +1,107 @@
/*
* 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.coroutines.data
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.memory.utils.StackFrameItem
import com.intellij.debugger.ui.impl.watch.MessageDescriptor
import com.intellij.debugger.ui.impl.watch.MethodsTracker
import com.intellij.debugger.ui.impl.watch.NodeDescriptorImpl
import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl
import com.intellij.debugger.ui.tree.render.DescriptorLabelListener
import com.intellij.icons.AllIcons
import com.sun.jdi.ObjectReference
import javax.swing.Icon
class CoroutineDescriptorImpl(val infoData: CoroutineInfoData) : NodeDescriptorImpl() {
lateinit var icon: Icon
override fun getName() = infoData.name
@Throws(EvaluateException::class)
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener): String {
val thread = infoData.thread
val name = thread?.name()?.substringBefore(" @${infoData.name}") ?: ""
val threadState = if (thread != null) DebuggerUtilsEx.getThreadStatusText(thread.status()) else ""
return "${infoData.name}: ${infoData.state}${if (name.isNotEmpty()) " on thread \"$name\":$threadState" else ""}"
}
override fun isExpandable() = infoData.state != CoroutineInfoData.State.CREATED
private fun calcIcon() = when {
infoData.isSuspended() -> AllIcons.Debugger.ThreadSuspended
infoData.isCreated() -> AllIcons.Debugger.ThreadStates.Idle
else -> AllIcons.Debugger.ThreadRunning
}
override fun setContext(context: EvaluationContextImpl?) {
icon = calcIcon()
}
}
class CreationFramesDescriptor(val frames: List<StackTraceElement>)
: MessageDescriptor("Coroutine creation stack trace", INFORMATION) {
override fun isExpandable() = true
}
/**
* Descriptor for suspend functions
*/
class SuspendStackFrameDescriptor(
val infoData: CoroutineInfoData,
val frame: StackTraceElement,
proxy: StackFrameProxyImpl,
val continuation: ObjectReference
) :
CoroutineStackFrameDescriptor(proxy) {
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String {
return with(frame) {
val pack = className.substringBeforeLast(".", "")
"$methodName:$lineNumber, ${className.substringAfterLast(".")} " +
if (pack.isNotEmpty()) "{$pack}" else ""
}
}
override fun getName() : String? = frame.methodName
}
/**
* For the case when no data inside frame is available
*/
class CoroutineCreatedStackFrameDescriptor(val frame: StackTraceElement, proxy: StackFrameProxyImpl) :
CoroutineStackFrameDescriptor(proxy) {
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String {
return with(frame) {
val pack = className.substringBeforeLast(".", "")
"$methodName:$lineNumber, ${className.substringAfterLast(".")} ${if (pack.isNotEmpty()) "{$pack}" else ""}"
}
}
override fun getName() = null
}
class AsyncStackFrameDescriptor(val infoData: CoroutineInfoData, val frame: StackFrameItem, proxy: StackFrameProxyImpl) :
CoroutineStackFrameDescriptor(proxy) {
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String {
return with(frame) {
val pack = path().substringBeforeLast(".", "")
"${method()}:${line()}, ${path().substringAfterLast(".")} ${if (pack.isNotEmpty()) "{$pack}" else ""}"
}
}
override fun getName() = frame.method()
}
open class CoroutineStackFrameDescriptor(proxy: StackFrameProxyImpl) : StackFrameDescriptorImpl(proxy, MethodsTracker()) {
override fun isExpandable() = false
}
@@ -0,0 +1,71 @@
/*
* 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.coroutines.data
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.impl.descriptors.data.DescriptorData
import com.intellij.debugger.impl.descriptors.data.DisplayKey
import com.intellij.debugger.impl.descriptors.data.SimpleDisplayKey
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.memory.utils.StackFrameItem
import com.intellij.debugger.ui.impl.watch.NodeDescriptorImpl
import com.intellij.openapi.project.Project
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.LookupContinuation
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
class CoroutineStackTraceData(infoData: CoroutineInfoData, proxy: StackFrameProxyImpl, evalContext: EvaluationContextImpl, val frame: StackTraceElement)
: CoroutineStackDescriptorData(infoData, proxy, evalContext) {
override fun hashCode() = frame.hashCode()
override fun equals(other: Any?) =
other is CoroutineStackTraceData && frame == other.frame
override fun createDescriptorImpl(project: Project): NodeDescriptorImpl {
val context = ExecutionContext(evalContext, proxy)
val lookupContinuation = LookupContinuation(context, frame)
// retrieve continuation only if suspend method
val continuation = lookupContinuation.findContinuation(infoData)
return if (continuation is ObjectReference)
SuspendStackFrameDescriptor(infoData, frame, proxy, continuation)
else
CoroutineCreatedStackFrameDescriptor(frame, proxy)
}
}
class CoroutineStackFrameData(infoData: CoroutineInfoData, proxy: StackFrameProxyImpl, evalContext: EvaluationContextImpl, val frame: StackFrameItem)
: CoroutineStackDescriptorData(infoData, proxy, evalContext) {
override fun createDescriptorImpl(project: Project): NodeDescriptorImpl =
AsyncStackFrameDescriptor(infoData, frame, proxy)
override fun hashCode() = frame.hashCode()
override fun equals(other: Any?) = other is CoroutineStackFrameData && frame == other.frame
}
abstract class CoroutineStackDescriptorData(val infoData: CoroutineInfoData, val proxy: StackFrameProxyImpl, val evalContext: EvaluationContextImpl)
: DescriptorData<NodeDescriptorImpl>() {
override fun getDisplayKey(): DisplayKey<NodeDescriptorImpl> = SimpleDisplayKey(infoData)
}
/**
* Describes coroutine itself in the tree (name: STATE), has children if stacktrace is not empty (state = CREATED)
*/
class CoroutineDescriptorData(private val infoData: CoroutineInfoData) : DescriptorData<CoroutineDescriptorImpl>() {
override fun createDescriptorImpl(project: Project) =
CoroutineDescriptorImpl(infoData)
override fun equals(other: Any?) = if (other !is CoroutineDescriptorData) false else infoData.name == other.infoData.name
override fun hashCode() = infoData.name.hashCode()
override fun getDisplayKey(): DisplayKey<CoroutineDescriptorImpl> = SimpleDisplayKey(infoData.name)
}
@@ -1,205 +0,0 @@
/*
* 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.coroutines
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.impl.descriptors.data.DescriptorData
import com.intellij.debugger.impl.descriptors.data.DisplayKey
import com.intellij.debugger.impl.descriptors.data.SimpleDisplayKey
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.memory.utils.StackFrameItem
import com.intellij.debugger.ui.impl.watch.MethodsTracker
import com.intellij.debugger.ui.impl.watch.NodeDescriptorImpl
import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl
import com.intellij.debugger.ui.tree.render.DescriptorLabelListener
import com.intellij.icons.AllIcons
import com.intellij.openapi.project.Project
import com.sun.jdi.ClassType
import com.sun.jdi.ObjectReference
import javaslang.control.Either
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
import javax.swing.Icon
/**
* Describes coroutine itself in the tree (name: STATE), has children if stacktrace is not empty (state = CREATED)
*/
class CoroutineData(private val state: CoroutineState) : DescriptorData<CoroutineDescriptorImpl>() {
override fun createDescriptorImpl(project: Project): CoroutineDescriptorImpl {
return CoroutineDescriptorImpl(state)
}
override fun equals(other: Any?) = if (other !is CoroutineData) false else state.name == other.state.name
override fun hashCode() = state.name.hashCode()
override fun getDisplayKey(): DisplayKey<CoroutineDescriptorImpl> = SimpleDisplayKey(state.name)
}
class CoroutineDescriptorImpl(val state: CoroutineState) : NodeDescriptorImpl() {
var suspendContext: SuspendContextImpl? = null
lateinit var icon: Icon
override fun getName(): String? {
return state.name
}
@Throws(EvaluateException::class)
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener): String {
val name = if (state.thread != null) state.thread.name().substringBefore(" @${state.name}") else ""
val threadState = if (state.thread != null) DebuggerUtilsEx.getThreadStatusText(state.thread.status()) else ""
return "${state.name}: ${state.state}${if (name.isNotEmpty()) " on thread \"$name\":$threadState" else ""}"
}
override fun isExpandable(): Boolean {
return state.state != CoroutineState.State.CREATED
}
private fun calcIcon() = when {
state.isSuspended -> AllIcons.Debugger.ThreadSuspended
state.state == CoroutineState.State.CREATED -> AllIcons.Debugger.ThreadStates.Idle
else -> AllIcons.Debugger.ThreadRunning
}
override fun setContext(context: EvaluationContextImpl?) {
icon = calcIcon()
}
}
class CoroutineStackFrameData private constructor(val state: CoroutineState, private val proxy: StackFrameProxyImpl) :
DescriptorData<NodeDescriptorImpl>() {
private lateinit var frame: Either<StackTraceElement, StackFrameItem>
constructor(state: CoroutineState, frame: StackTraceElement, proxy: StackFrameProxyImpl) : this(state, proxy) {
this.frame = Either.left(frame)
}
constructor(state: CoroutineState, frameItem: StackFrameItem, proxy: StackFrameProxyImpl) : this(state, proxy) {
this.frame = Either.right(frameItem)
}
override fun hashCode(): Int {
return if (frame.isLeft) frame.left.hashCode() else frame.get().hashCode()
}
override fun equals(other: Any?): Boolean {
return other is CoroutineStackFrameData && frame == other.frame
}
/**
* Returns [EmptyStackFrameDescriptor], [SuspendStackFrameDescriptor]
* or [AsyncStackFrameDescriptor] according to current frame
*/
override fun createDescriptorImpl(project: Project): NodeDescriptorImpl {
val isLeft = frame.isLeft
if (!isLeft) return AsyncStackFrameDescriptor(state, frame.get(), proxy)
// check whether last fun is suspend fun
val frame = frame.left
val suspendContext =
DebuggerManagerEx.getInstanceEx(project).context.suspendContext ?: return EmptyStackFrameDescriptor(
frame,
proxy
)
val suspendProxy = suspendContext.frameProxy ?: return EmptyStackFrameDescriptor(
frame,
proxy
)
val evalContext = EvaluationContextImpl(suspendContext, suspendContext.frameProxy)
val context = ExecutionContext(evalContext, suspendProxy)
val clazz = context.findClass(frame.className) as ClassType
val method = clazz.methodsByName(frame.methodName).last {
val loc = it.location().lineNumber()
loc < 0 && frame.lineNumber < 0 || loc > 0 && loc <= frame.lineNumber
} // pick correct method if an overloaded one is given
return if ("Lkotlin/coroutines/Continuation;)" in method.signature() ||
method.name() == "invokeSuspend" &&
method.signature() == "(Ljava/lang/Object;)Ljava/lang/Object;" // suspend fun or invokeSuspend
) {
val continuation = state.getContinuation(frame, context)
if (continuation == null) EmptyStackFrameDescriptor(
frame,
proxy
) else
SuspendStackFrameDescriptor(
state,
frame,
proxy,
continuation
)
} else EmptyStackFrameDescriptor(frame, proxy)
}
override fun getDisplayKey(): DisplayKey<NodeDescriptorImpl> = SimpleDisplayKey(state)
}
/**
* Descriptor for suspend functions
*/
class SuspendStackFrameDescriptor(
val state: CoroutineState,
val frame: StackTraceElement,
proxy: StackFrameProxyImpl,
val continuation: ObjectReference
) :
StackFrameDescriptorImpl(proxy, MethodsTracker()) {
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String {
return with(frame) {
val pack = className.substringBeforeLast(".", "")
"$methodName:$lineNumber, ${className.substringAfterLast(".")} " +
if (pack.isNotEmpty()) "{$pack}" else ""
}
}
override fun isExpandable() = false
override fun getName(): String {
return frame.methodName
}
}
class AsyncStackFrameDescriptor(val state: CoroutineState, val frame: StackFrameItem, proxy: StackFrameProxyImpl) :
StackFrameDescriptorImpl(proxy, MethodsTracker()) {
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String {
return with(frame) {
val pack = path().substringBeforeLast(".", "")
"${method()}:${line()}, ${path().substringAfterLast(".")} ${if (pack.isNotEmpty()) "{$pack}" else ""}"
}
}
override fun getName(): String {
return frame.method()
}
override fun isExpandable(): Boolean = false
}
/**
* For the case when no data inside frame is available
*/
class EmptyStackFrameDescriptor(val frame: StackTraceElement, proxy: StackFrameProxyImpl) :
StackFrameDescriptorImpl(proxy, MethodsTracker()) {
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String {
return with(frame) {
val pack = className.substringBeforeLast(".", "")
"$methodName:$lineNumber, ${className.substringAfterLast(".")} ${if (pack.isNotEmpty()) "{$pack}" else ""}"
}
}
override fun getName() = null
override fun isExpandable() = false
}
class CreationFramesDescriptor(val frames: List<StackTraceElement>) : NodeDescriptorImpl() {
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?) = name
override fun setContext(context: EvaluationContextImpl?) {}
override fun getName() = "Coroutine creation stack trace"
override fun isExpandable() = true
}
@@ -0,0 +1,255 @@
/*
* 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.coroutines.proxy
import com.intellij.debugger.engine.DebuggerManagerThreadImpl
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.impl.PrioritizedTask
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.xdebugger.XDebugProcess
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.coroutines.command.CoroutineBuilder
import org.jetbrains.kotlin.idea.debugger.coroutines.view.CoroutineInfoCache
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData
import org.jetbrains.kotlin.idea.debugger.coroutines.util.logger
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
fun SuspendContextImpl.createEvaluationContext() =
EvaluationContextImpl(this, this.frameProxy)
class CoroutinesDebugProbesProxy(val suspendContext: SuspendContextImpl) {
private val log by logger
// @TODO refactor to extract initialization logic
private var executionContext: ExecutionContext = ExecutionContext(
EvaluationContextImpl(suspendContext, suspendContext.frameProxy),
suspendContext.frameProxy as StackFrameProxyImpl)
// might want to use inner class but also having to monitor order of fields
private var refs: ProcessReferences = ProcessReferences(executionContext)
companion object {
private const val DEBUG_PACKAGE = "kotlinx.coroutines.debug"
}
@Synchronized
@Suppress("unused")
fun install() =
executionContext.invokeMethodAsVoid(refs.instance, "install")
@Synchronized
@Suppress("unused")
fun uninstall() =
executionContext.invokeMethodAsVoid(refs.instance, "uninstall")
/**
* Invokes DebugProbes from debugged process's classpath and returns states of coroutines
* Should be invoked on debugger manager thread
*/
@Synchronized
fun dumpCoroutines() : CoroutineInfoCache {
val coroutineInfoCache = CoroutineInfoCache()
try {
val infoList = dump()
coroutineInfoCache.ok(infoList)
} catch (e: Throwable) {
log.error("Exception is thrown by calling dumpCoroutines.", e)
coroutineInfoCache.fail()
}
return coroutineInfoCache
}
private fun dump(): List<CoroutineInfoData> {
val coroutinesInfo = dumpCoroutinesInfo() ?: return emptyList()
executionContext.keepReference(coroutinesInfo)
val size = sizeOf(coroutinesInfo)
return MutableList(size) {
val elem = getElementFromList(coroutinesInfo, it)
fetchCoroutineState(elem)
}
}
private fun dumpCoroutinesInfo() =
executionContext.invokeMethodAsObject(refs.instance, refs.dumpMethod)
fun frameBuilder() = CoroutineBuilder(suspendContext)
private fun getElementFromList(instance: ObjectReference, num: Int) =
executionContext.invokeMethod(
instance, refs.getRef,
listOf(executionContext.vm.virtualMachine.mirrorOf(num))
) as ObjectReference
private fun fetchCoroutineState(instance: ObjectReference) : CoroutineInfoData {
val name = getName(instance)
val state = getState(instance)
val thread = getLastObservedThread(instance, refs.lastObservedThreadFieldRef)
val lastObservedFrameFieldRef = instance.getValue(refs.lastObservedFrameFieldRef) as? ObjectReference
return CoroutineInfoData(
name,
CoroutineInfoData.State.valueOf(state),
getThreadName(instance),
getThreadState(instance),
getStackTrace(instance),
thread,
lastObservedFrameFieldRef
)
}
private fun getThreadName(instance: ObjectReference) : String {
return "thread name"
}
private fun getThreadState(instance: ObjectReference) : Int {
return 1
}
private fun getName(
info: ObjectReference // CoroutineInfo instance
): String {
// equals to `coroutineInfo.context.get(CoroutineName).name`
val coroutineContextInst = executionContext.invokeMethod(
info,
refs.getContextRef,
emptyList()
) as? ObjectReference ?: throw IllegalArgumentException("Coroutine context must not be null")
val coroutineName = executionContext.invokeMethod(
coroutineContextInst,
refs.getContextElement, listOf(refs.keyFieldValueRef)
) as? ObjectReference
// If the coroutine doesn't have a given name, CoroutineContext.get(CoroutineName) returns null
val name = if (coroutineName != null) (executionContext.invokeMethod(
coroutineName,
refs.getNameRef,
emptyList()
) as StringReference).value() else "coroutine"
val id = (info.getValue(refs.sequenceNumberFieldRef) as LongValue).value()
return "$name#$id"
}
private fun getState(
info: ObjectReference // CoroutineInfo instance
): String {
// equals to `stringState = coroutineInfo.state.toString()`
val state = executionContext.invokeMethod(info, refs.getStateRef, emptyList()) as ObjectReference
return (executionContext.invokeMethod(state, refs.toString, emptyList()) as StringReference).value()
}
private fun getLastObservedThread(
info: ObjectReference, // CoroutineInfo instance
threadRef: Field // reference to lastObservedThread
): ThreadReference? = info.getValue(threadRef) as? ThreadReference
/**
* Returns list of stackTraceElements for the given CoroutineInfo's [ObjectReference]
*/
private fun getStackTrace(
info: ObjectReference
): List<StackTraceElement> {
val frameList = lastObservedStackTrace(info)
val tmpList = mutableListOf<StackTraceElement>()
for(it in 0 until sizeOf(frameList)) {
val frame = getElementFromList(frameList, it)
val ste = newStackTraceElement(frame)
tmpList.add(ste)
}
val mergedFrameList = enhanceStackTraceWithThreadDump(listOf(info, frameList))
val size = sizeOf(mergedFrameList)
val list = mutableListOf<StackTraceElement>()
for (it in 0 until size) {
val frame = getElementFromList(mergedFrameList, it)
val ste = newStackTraceElement(frame)
list.add(// 0, // add in the beginning // @TODO what's the point?
ste)
}
return list
}
private fun newStackTraceElement(frame: ObjectReference) =
StackTraceElement(
fetchClassName(frame),
fetchMethodName(frame),
fetchFileName(frame),
fetchLine(frame)
)
private fun fetchLine(instance: ObjectReference) =
(instance.getValue(refs.lineNumberFieldRef) as? IntegerValue)?.value() ?: -1
private fun fetchFileName(instance: ObjectReference) =
(instance.getValue(refs.fileNameFieldRef) as? StringReference)?.value() ?: ""
private fun fetchMethodName(instance: ObjectReference) =
(instance.getValue(refs.methodNameFieldRef) as? StringReference)?.value() ?: ""
private fun fetchClassName(instance: ObjectReference) =
(instance.getValue(refs.declaringClassFieldRef) as? StringReference)?.value() ?: ""
private fun lastObservedStackTrace(instance: ObjectReference) =
executionContext.invokeMethod(instance, refs.lastObservedStackTraceRef, emptyList()) as ObjectReference
private fun enhanceStackTraceWithThreadDump(args: List<ObjectReference>) =
executionContext.invokeMethod(
refs.debugProbesImplInstance,
refs.enhanceStackTraceWithThreadDumpRef, args) as ObjectReference
private fun sizeOf(args: ObjectReference): Int =
(executionContext.invokeMethod(args, refs.sizeRef, emptyList()) as IntegerValue).value()
/**
* @TODO refactor later
* Holds ClassTypes, Methods, ObjectReferences and Fields for a particular jvm
*/
class ProcessReferences(executionContext: ExecutionContext) {
// kotlinx.coroutines.debug.DebugProbes instance and methods
val debugProbesClsRef = executionContext.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType
val debugProbesImplClsRef = executionContext.findClass("$DEBUG_PACKAGE.internal.DebugProbesImpl") as ClassType
val coroutineNameClsRef = executionContext.findClass("kotlinx.coroutines.CoroutineName") as ClassType
val classClsRef = executionContext.findClass("java.lang.Object") as ClassType
val debugProbesImplInstance = with(debugProbesImplClsRef) { getValue(fieldByName("INSTANCE")) as ObjectReference }
val enhanceStackTraceWithThreadDumpRef: Method = debugProbesImplClsRef
.methodsByName("enhanceStackTraceWithThreadDump").single()
val dumpMethod: Method = debugProbesClsRef.concreteMethodByName("dumpCoroutinesInfo", "()Ljava/util/List;")
val instance = with(debugProbesClsRef) { getValue(fieldByName("INSTANCE")) as ObjectReference }
// CoroutineInfo
val coroutineInfoClsRef = executionContext.findClass("$DEBUG_PACKAGE.CoroutineInfo") as ClassType
val coroutineContextClsRef = executionContext.findClass("kotlin.coroutines.CoroutineContext") as InterfaceType
val getStateRef: Method = coroutineInfoClsRef.concreteMethodByName("getState", "()Lkotlinx/coroutines/debug/State;")
val getContextRef: Method = coroutineInfoClsRef.concreteMethodByName("getContext", "()Lkotlin/coroutines/CoroutineContext;")
val sequenceNumberFieldRef: Field = coroutineInfoClsRef.fieldByName("sequenceNumber")
val lastObservedStackTraceRef: Method = coroutineInfoClsRef.methodsByName("lastObservedStackTrace").single()
val getContextElement: Method = coroutineContextClsRef.methodsByName("get").single()
val getNameRef: Method = coroutineNameClsRef.methodsByName("getName").single()
val keyFieldRef = coroutineNameClsRef.fieldByName("Key")
val toString: Method = classClsRef.concreteMethodByName("toString", "()Ljava/lang/String;")
val lastObservedThreadFieldRef: Field = coroutineInfoClsRef.fieldByName("lastObservedThread")
val lastObservedFrameFieldRef: Field = coroutineInfoClsRef.fieldByName("lastObservedFrame") // continuation
// Methods for list
val listClsRef = executionContext.findClass("java.util.List") as InterfaceType
val sizeRef: Method = listClsRef.methodsByName("size").single()
val getRef: Method = listClsRef.methodsByName("get").single()
val stackTraceElementClsRef = executionContext.findClass("java.lang.StackTraceElement") as ClassType
// for StackTraceElement
val methodNameFieldRef: Field = stackTraceElementClsRef.fieldByName("methodName")
val declaringClassFieldRef: Field = stackTraceElementClsRef.fieldByName("declaringClass")
val fileNameFieldRef: Field = stackTraceElementClsRef.fieldByName("fileName")
val lineNumberFieldRef: Field = stackTraceElementClsRef.fieldByName("lineNumber")
// value
val keyFieldValueRef = coroutineNameClsRef.getValue(keyFieldRef) as ObjectReference
}
}
@@ -3,52 +3,43 @@
* 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.coroutines
package org.jetbrains.kotlin.idea.debugger.coroutines.proxy
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
import org.jetbrains.kotlin.idea.debugger.isSubtype
/**
* Represents state of a coroutine.
* @see `kotlinx.coroutines.debug.CoroutineInfo`
*/
class CoroutineState(
val name: String,
val state: State,
val thread: ThreadReference? = null,
val stackTrace: List<StackTraceElement>,
val frame: ObjectReference?
) {
val isSuspended: Boolean = state == State.SUSPENDED
val isEmptyStackTrace: Boolean by lazy { stackTrace.isEmpty() }
val stringStackTrace: String by lazy {
buildString {
appendln("\"$name\", state: $state")
stackTrace.forEach {
appendln("\t$it")
}
}
class LookupContinuation(val context: ExecutionContext, val frame: StackTraceElement) {
private fun suspendOrInvokeSuspend(method: Method): Boolean =
"Lkotlin/coroutines/Continuation;)" in method.signature() ||
(method.name() == "invokeSuspend" && method.signature() == "(Ljava/lang/Object;)Ljava/lang/Object;") // suspend fun or invokeSuspend
private fun findMethod() : Method {
val clazz = context.findClass(frame.className) as ClassType
val method = clazz.methodsByName(frame.methodName).last {
val loc = it.location().lineNumber()
loc < 0 && frame.lineNumber < 0 || loc > 0 && loc <= frame.lineNumber
} // pick correct method if an overloaded one is given
return method
}
fun isApplicable(): Boolean {
val method = findMethod()
return suspendOrInvokeSuspend(method)
}
/**
* Finds previous Continuation for this Continuation (completion field in BaseContinuationImpl)
* @return null if given ObjectReference is not a BaseContinuationImpl instance or completion is null
*/
private fun getNextFrame(continuation: ObjectReference, context: ExecutionContext): ObjectReference? {
val type = continuation.type() as ClassType
if (!type.isSubtype("kotlin.coroutines.jvm.internal.BaseContinuationImpl")) return null
val next = type.concreteMethodByName("getCompletion", "()Lkotlin/coroutines/Continuation;")
return context.invokeMethod(continuation, next, emptyList()) as? ObjectReference
}
/**
* Find continuation for the [stackTraceElement]
* Find continuation for the [frame]
* Gets current CoroutineInfo.lastObservedFrame and finds next frames in it until null or needed stackTraceElement is found
* @return null if matching continuation is not found or is not BaseContinuationImpl
*/
fun getContinuation(stackTraceElement: StackTraceElement, context: ExecutionContext): ObjectReference? {
var continuation = frame ?: return null
fun findContinuation(infoData: CoroutineInfoData): ObjectReference? {
if (!isApplicable())
return null
var continuation = infoData.frame ?: return null
val baseType = "kotlin.coroutines.jvm.internal.BaseContinuationImpl"
val getTrace = (continuation.type() as ClassType).concreteMethodByName(
"getStackTraceElement",
@@ -71,7 +62,7 @@ class CoroutineState(
}
while (continuation.type().isSubtype(baseType)
&& (stackTraceElement.className != className() || stackTraceElement.lineNumber != lineNumber())
&& (frame.className != className() || frame.lineNumber != lineNumber())
) {
// while continuation is BaseContinuationImpl and it's frame equals to the current
continuation = getNextFrame(continuation, context) ?: return null
@@ -79,9 +70,15 @@ class CoroutineState(
return if (continuation.type().isSubtype(baseType)) continuation else null
}
enum class State {
RUNNING,
SUSPENDED,
CREATED
/**
* Finds previous Continuation for this Continuation (completion field in BaseContinuationImpl)
* @return null if given ObjectReference is not a BaseContinuationImpl instance or completion is null
*/
private fun getNextFrame(continuation: ObjectReference, context: ExecutionContext): ObjectReference? {
val type = continuation.type() as ClassType
if (!type.isSubtype("kotlin.coroutines.jvm.internal.BaseContinuationImpl")) return null
val next = type.concreteMethodByName("getCompletion", "()Lkotlin/coroutines/Continuation;")
return context.invokeMethod(continuation, next, emptyList()) as? ObjectReference
}
}
@@ -0,0 +1,32 @@
/*
* 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.coroutines.proxy
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.engine.events.DebuggerCommandImpl
import com.intellij.debugger.impl.PrioritizedTask
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.util.Computable
class ManagerThreadExecutor(val debugProcess: DebugProcessImpl, val priority: PrioritizedTask.Priority = PrioritizedTask.Priority.NORMAL) {
fun schedule(f: () -> Unit) {
val runnable = object : Runnable {
override fun run() {f()}
}
debugProcess.managerThread.schedule(priority, runnable)
}
fun schedule(f: DebuggerCommandImpl) {
debugProcess.managerThread.schedule(f)
}
}
class ApplicationThreadExecutor() {
fun <T> invoke(f: () -> T) : T {
return ApplicationManager.getApplication().runReadAction(Computable(f))
}
}
@@ -0,0 +1,107 @@
/*
* 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.coroutines.util
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.memory.utils.StackFrameItem
import com.intellij.debugger.ui.impl.watch.MethodsTracker
import com.intellij.debugger.ui.impl.watch.NodeDescriptorImpl
import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Computable
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.xdebugger.XDebuggerUtil
import com.intellij.xdebugger.XSourcePosition
import com.sun.jdi.ClassType
import javaslang.control.Either
import org.jetbrains.kotlin.idea.debugger.coroutines.data.AsyncStackFrameDescriptor
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData
import org.jetbrains.kotlin.idea.debugger.coroutines.data.SuspendStackFrameDescriptor
import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.ApplicationThreadExecutor
import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.LookupContinuation
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
fun getPosition(stackTraceElement: StackTraceElement, project: Project): XSourcePosition? {
val psiFacade = JavaPsiFacade.getInstance(project)
val psiClass = ApplicationThreadExecutor().invoke {
@Suppress("DEPRECATION")
psiFacade.findClass(
stackTraceElement.className.substringBefore("$"), // find outer class, for which psi exists TODO
GlobalSearchScope.everythingScope(project))
}
val classFile = psiClass?.containingFile?.virtualFile
// to convert to 0-based line number or '-1' to do not move
val lineNumber = if (stackTraceElement.lineNumber > 0) stackTraceElement.lineNumber - 1 else return null
return XDebuggerUtil.getInstance().createPosition(classFile, lineNumber)
}
/*
private fun buildSuspendStackFrameChildren(descriptor: SuspendStackFrameDescriptor, frame: StackTraceElement, project: Project) {
val context = DebuggerManagerEx.getInstanceEx(project).context
val pos = getPosition(frame) ?: return
context.debugProcess?.managerThread?.schedule(object : SuspendContextCommandImpl(context.suspendContext) {
override fun contextAction() {
val (stack, stackFrame) = createSyntheticStackFrame(descriptor, pos) ?: return
val action: () -> Unit = { context.debuggerSession?.xDebugSession?.setCurrentStackFrame(stack, stackFrame) }
ApplicationManager.getApplication()
.invokeLater(action, ModalityState.stateForComponent(this@CoroutinesDebuggerTree))
}
})
}
private fun buildAsyncStackFrameChildren(descriptor: AsyncStackFrameDescriptor, process: DebugProcessImpl) {
process.managerThread?.schedule(object : DebuggerCommandImpl() {
override fun action() {
val context = DebuggerManagerEx.getInstanceEx(project).context
val proxy = ThreadReferenceProxyImpl(
process.virtualMachineProxy,
descriptor.state.thread // is not null because it's a running coroutine
)
val executionStack = JavaExecutionStack(proxy, process, false)
executionStack.initTopFrame()
val frame = descriptor.frame.createFrame(process)
DebuggerUIUtil.invokeLater {
context.debuggerSession?.xDebugSession?.setCurrentStackFrame(
executionStack,
frame
)
}
}
})
}
private fun buildEmptyStackFrameChildren(descriptor: EmptyStackFrameDescriptor, project: Project) {
val position = getPosition(descriptor.frame) ?: return
val context = DebuggerManagerEx.getInstanceEx(project).context
val suspendContext = context.suspendContext ?: return
val proxy = suspendContext.thread ?: return
context.debugProcess?.managerThread?.schedule(object : DebuggerCommandImpl() {
override fun action() {
val executionStack =
JavaExecutionStack(proxy, context.debugProcess!!, false)
executionStack.initTopFrame()
val frame = SyntheticStackFrame(descriptor, emptyList(), position)
val action: () -> Unit =
{ context.debuggerSession?.xDebugSession?.setCurrentStackFrame(executionStack, frame) }
ApplicationManager.getApplication()
.invokeLater(action, ModalityState.stateForComponent(this@CoroutinesDebuggerTree))
}
})
}
*/
class EmptyStackFrameDescriptor(val frame: StackTraceElement, proxy: StackFrameProxyImpl) :
StackFrameDescriptorImpl(proxy, MethodsTracker()) {
}
@@ -0,0 +1,40 @@
/*
* 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.coroutines.util
import com.intellij.openapi.diagnostic.Logger
import com.intellij.xdebugger.XDebugSession
import com.intellij.xdebugger.XDebugSessionListener
import javax.swing.Icon
import javax.swing.JComponent
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
interface CreateContentParamsProvider {
fun createContentParams() : CreateContentParams
}
data class CreateContentParams(val id: String, val component: JComponent, val displayName: String, val icon: Icon?, val parentComponent: JComponent)
interface XDebugSessionListenerProvider {
fun debugSessionListener(session: XDebugSession) : XDebugSessionListener
}
/**
* Logger instantiation sample: 'val log by logger'
*/
val logger: ReadOnlyProperty<Any, Logger> get() = LoggerDelegate()
class LoggerDelegate : ReadOnlyProperty<Any, Logger> {
lateinit var logger: Logger
override fun getValue(thisRef: Any, property: KProperty<*>): Logger {
if (!::logger.isInitialized)
logger = Logger.getInstance(thisRef.javaClass)
return logger
}
}
@@ -0,0 +1,35 @@
/*
* 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.coroutines.util;
import com.intellij.xdebugger.XSourcePosition;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Utils {
public static Object callPrivate(
Object instance,
String methodName,
Object... args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
Class[] classes = new Class[args.length];
for (int i = 0; i < args.length; i++) {
if(args[i] instanceof ReflectionSpecificType)
classes[i] = ((ReflectionSpecificType) args[i]).specificClass();
else
classes[i] = args[i].getClass();
}
Method method = instance.getClass().getDeclaredMethod(methodName, classes);
method.setAccessible(true);
Object r = method.invoke(instance, args);
return r;
}
public interface ReflectionSpecificType {
Class specificClass();
}
}
@@ -3,7 +3,7 @@
* 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.coroutines
package org.jetbrains.kotlin.idea.debugger.coroutines.view
import com.intellij.codeInsight.highlighting.HighlightManager
import com.intellij.execution.ui.ConsoleView
@@ -32,6 +32,7 @@ import com.intellij.ui.components.JBList
import com.intellij.unscramble.AnalyzeStacktraceUtil
import com.intellij.util.PlatformIcons
import com.intellij.util.ui.EmptyIcon
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData
import java.awt.BorderLayout
import java.awt.Color
import java.awt.datatransfer.StringSelection
@@ -42,10 +43,10 @@ import javax.swing.event.DocumentEvent
/**
* Panel with dump of coroutines
*/
class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActions: DefaultActionGroup, val dump: List<CoroutineState>) :
class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActions: DefaultActionGroup, val dump: List<CoroutineInfoData>) :
JPanel(BorderLayout()), DataProvider {
private var exporterToTextFile: ExporterToTextFile
private var mergedDump = ArrayList<CoroutineState>()
private var mergedDump = ArrayList<CoroutineInfoData>()
val filterField = SearchTextField()
val filterPanel = JPanel(BorderLayout())
private val coroutinesList = JBList(DefaultListModel<Any>())
@@ -71,7 +72,7 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
addListSelectionListener {
val index = selectedIndex
if (index >= 0) {
val selection = model.getElementAt(index) as CoroutineState
val selection = model.getElementAt(index) as CoroutineInfoData
AnalyzeStacktraceUtil.printStacktrace(consoleView, selection.stringStackTrace)
} else {
AnalyzeStacktraceUtil.printStacktrace(consoleView, "")
@@ -80,7 +81,8 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
}
}
exporterToTextFile = MyToFileExporter(project, dump)
exporterToTextFile =
MyToFileExporter(project, dump)
val filterAction = FilterAction().apply {
registerCustomShortcutSet(
@@ -90,7 +92,9 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
}
toolbarActions.apply {
add(filterAction)
add(CopyToClipboardAction(dump, project))
add(
CopyToClipboardAction(dump, project)
)
add(ActionManager.getInstance().getAction(IdeActions.ACTION_EXPORT_TO_TEXT_FILE))
add(MergeStackTracesAction())
}
@@ -174,18 +178,18 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
override fun getData(dataId: String): Any? = if (PlatformDataKeys.EXPORTER_TO_TEXT_FILE.`is`(dataId)) exporterToTextFile else null
private fun getCoroutineStateIcon(state: CoroutineState): Icon {
return when (state.state) {
CoroutineState.State.RUNNING -> LayeredIcon(AllIcons.Actions.Resume, Daemon_sign)
CoroutineState.State.SUSPENDED -> AllIcons.Actions.Pause
private fun getCoroutineStateIcon(infoData: CoroutineInfoData): Icon {
return when (infoData.state) {
CoroutineInfoData.State.RUNNING -> LayeredIcon(AllIcons.Actions.Resume, Daemon_sign)
CoroutineInfoData.State.SUSPENDED -> AllIcons.Actions.Pause
else -> EmptyIcon.create(6)
}
}
private fun getAttributes(state: CoroutineState): SimpleTextAttributes {
private fun getAttributes(infoData: CoroutineInfoData): SimpleTextAttributes {
return when {
state.isSuspended -> SimpleTextAttributes.GRAY_ATTRIBUTES
state.isEmptyStackTrace -> SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, Color.GRAY.brighter())
infoData.isSuspended() -> SimpleTextAttributes.GRAY_ATTRIBUTES
infoData.isEmptyStackTrace() -> SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, Color.GRAY.brighter())
else -> SimpleTextAttributes.REGULAR_ATTRIBUTES
}
@@ -194,7 +198,7 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
private inner class CoroutineListCellRenderer : ColoredListCellRenderer<Any>() {
override fun customizeCellRenderer(list: JList<*>, value: Any, index: Int, selected: Boolean, hasFocus: Boolean) {
val state = value as CoroutineState
val state = value as CoroutineInfoData
icon = getCoroutineStateIcon(state)
val attrs = getAttributes(state)
append(state.name + " (", attrs)
@@ -246,7 +250,7 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
}
}
private class CopyToClipboardAction(private val myCoroutinesDump: List<CoroutineState>, private val myProject: Project) :
private class CopyToClipboardAction(private val myCoroutinesDump: List<CoroutineInfoData>, private val myProject: Project) :
DumbAwareAction("Copy to Clipboard", "Copy whole coroutine dump to clipboard", PlatformIcons.COPY_ICON) {
override fun actionPerformed(e: AnActionEvent) {
@@ -268,17 +272,17 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
private class MyToFileExporter(
private val myProject: Project,
private val states: List<CoroutineState>
private val infoData: List<CoroutineInfoData>
) : ExporterToTextFile {
override fun getReportText() = buildString {
for (state in states)
for (state in infoData)
append(state.stringStackTrace).append("\n\n")
}
override fun getDefaultFilePath() = (myProject.basePath ?: "") + File.separator + defaultReportFileName
override fun canExport() = states.isNotEmpty()
override fun canExport() = infoData.isNotEmpty()
private val defaultReportFileName = "coroutines_report.txt"
}
@@ -0,0 +1,68 @@
/*
* 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.coroutines.view
import com.intellij.openapi.application.ApplicationManager
import com.intellij.xdebugger.XDebugSession
import com.intellij.xdebugger.XDebugSessionListener
import com.intellij.xdebugger.frame.XSuspendContext
import com.intellij.xdebugger.impl.ui.DebuggerUIUtil
import org.jetbrains.kotlin.idea.debugger.coroutines.util.logger
class CoroutineViewDebugSessionListener(
private val session: XDebugSession,
private val xCoroutineView: XCoroutineView
) : XDebugSessionListener {
val log by logger
override fun sessionPaused() {
val suspendContext = session.suspendContext ?: return requestClear()
xCoroutineView.forceClear()
renew(suspendContext)
}
override fun sessionResumed() {
val suspendContext = session.suspendContext ?: return requestClear()
log.warn("sessionResumed ${session}")
renew(suspendContext)
}
override fun sessionStopped() {
val suspendContext = session.suspendContext ?: return requestClear()
log.warn("sessionStopped ${session}")
renew(suspendContext)
}
override fun stackFrameChanged() {
val suspendContext = session.suspendContext ?: return requestClear()
log.warn("stackFrameChanged ${session}")
renew(suspendContext)
}
override fun beforeSessionResume() {
log.warn("beforeSessionResume ${session}")
}
override fun settingsChanged() {
val suspendContext = session.suspendContext ?: return requestClear()
log.warn("settingsChanged ${session}")
renew(suspendContext)
}
fun renew(suspendContext: XSuspendContext) {
DebuggerUIUtil.invokeLater {
xCoroutineView.panel.tree.setRoot(xCoroutineView.createRoot(suspendContext), false)
}
}
fun requestClear() {
if (ApplicationManager.getApplication().isUnitTestMode) { // no delay in tests
xCoroutineView.clear()
} else {
xCoroutineView.alarm.cancelAndRequest()
}
}
}
@@ -0,0 +1,94 @@
/*
* 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.coroutines.view
import com.intellij.debugger.DebuggerBundle
import com.intellij.debugger.engine.events.DebuggerCommandImpl
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.impl.DebuggerSession
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.ui.impl.ThreadsDebuggerTree
import com.intellij.debugger.ui.impl.watch.*
import com.intellij.debugger.ui.tree.StackFrameDescriptor
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.idea.debugger.coroutines.command.CoroutineBuildCreationFrameCommand
import org.jetbrains.kotlin.idea.debugger.coroutines.command.CoroutineBuildFrameCommand
import org.jetbrains.kotlin.idea.debugger.coroutines.command.RefreshCoroutinesTreeCommand
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineDescriptorImpl
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CreationFramesDescriptor
import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.ManagerThreadExecutor
import org.jetbrains.kotlin.idea.debugger.coroutines.util.logger
import java.util.*
/**
* Tree of coroutines for [CoroutinesPanel]
*/
class CoroutinesDebuggerTree(project: Project) : ThreadsDebuggerTree(project) {
private val log by logger
// called on every step/frame
override fun build(context: DebuggerContextImpl) {
val session = context.debuggerSession
val command = RefreshCoroutinesTreeCommand(context, this)
val debuggerSessionState = session?.state ?: DebuggerSession.State.DISPOSED
if (ApplicationManager.getApplication().isUnitTestMode || debuggerSessionState in EnumSet.of(DebuggerSession.State.PAUSED, DebuggerSession.State.RUNNING)) {
showMessage(MessageDescriptor.EVALUATING)
ManagerThreadExecutor(context.debugProcess!!).schedule(command)
} else {
showMessage(session?.stateDescription ?: DebuggerBundle.message("status.debug.stopped"))
}
}
override fun getBuildNodeCommand(node: DebuggerTreeNodeImpl): DebuggerCommandImpl? {
return when(val descriptor = node.descriptor) {
is CoroutineDescriptorImpl ->
CoroutineBuildFrameCommand(node, descriptor, myNodeManager, debuggerContext)
is CreationFramesDescriptor ->
CoroutineBuildCreationFrameCommand(node, descriptor, myNodeManager, debuggerContext)
else -> null
}
}
override fun createNodeManager(project: Project): NodeManagerImpl {
return object : NodeManagerImpl(project, this) {
override fun getContextKey(frame: StackFrameProxyImpl?): String? {
return "CoroutinesView"
}
}
}
override fun isExpandable(node: DebuggerTreeNodeImpl): Boolean {
val descriptor = node.descriptor
return if (descriptor is StackFrameDescriptor) false else descriptor.isExpandable
}
}
class CoroutineInfoCache(val cache: MutableList<CoroutineInfoData> = mutableListOf(), var state: CacheState = CacheState.INIT
) {
fun ok(infoList: List<CoroutineInfoData>) {
cache.clear()
cache.addAll(infoList)
state = CacheState.OK
}
fun fail() {
cache.clear()
state = CacheState.FAIL
}
fun isOk() : Boolean {
return state == CacheState.OK
}
}
enum class CacheState() {
OK,FAIL,INIT
}
@@ -0,0 +1,43 @@
/*
* 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.
*/
@file:Suppress("DEPRECATION")
package org.jetbrains.kotlin.idea.debugger.coroutines.view
import com.intellij.debugger.actions.DebuggerActions
import com.intellij.debugger.impl.DebuggerStateManager
import com.intellij.debugger.ui.impl.ThreadsPanel
import com.intellij.debugger.ui.impl.watch.DebuggerTree
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineDebuggerActions
/**
* Added into ui in [CoroutineProjectConnectionListener.registerCoroutinesPanel]
*/
class CoroutinesPanel(project: Project, stateManager: DebuggerStateManager) : ThreadsPanel(project, stateManager) {
override fun createTreeView(): DebuggerTree {
return CoroutinesDebuggerTree(project)
}
override fun createPopupMenu(): ActionPopupMenu {
val group = ActionManager.getInstance().getAction(DebuggerActions.THREADS_PANEL_POPUP) as DefaultActionGroup
return ActionManager.getInstance().createActionPopupMenu(
CoroutineDebuggerActions.COROUTINE_PANEL_POPUP, group)
}
override fun getData(dataId: String): Any? {
return if (helpDataId(dataId)) HELP_ID else super.getData(dataId)
}
private fun helpDataId(dataId: String): Boolean = PlatformDataKeys.HELP_ID.`is`(dataId)
companion object {
val HELP_ID = "debugging.debugCoroutines"
}
}
@@ -0,0 +1,263 @@
/*
* 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.coroutines.view
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.JavaDebugProcess
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.settings.ThreadsViewSettings
import com.intellij.icons.AllIcons
import com.intellij.openapi.Disposable
import com.intellij.openapi.project.Project
import com.intellij.ui.OnePixelSplitter
import com.intellij.ui.SimpleColoredComponent
import com.intellij.ui.SimpleColoredText
import com.intellij.ui.SimpleTextAttributes
import com.intellij.util.SingleAlarm
import com.intellij.xdebugger.XDebugSession
import com.intellij.xdebugger.XSourcePosition
import com.intellij.xdebugger.frame.*
import com.intellij.xdebugger.frame.presentation.XRegularValuePresentation
import com.intellij.xdebugger.frame.presentation.XValuePresentation
import com.intellij.xdebugger.impl.ui.DebuggerUIUtil
import com.intellij.xdebugger.impl.ui.XDebuggerUIConstants
import com.intellij.xdebugger.impl.ui.tree.XDebuggerTree
import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreePanel
import com.intellij.xdebugger.impl.ui.tree.nodes.XValueContainerNode
import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl
import com.sun.jdi.Location
import com.sun.jdi.ReferenceType
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineDebuggerContentInfo
import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineDebuggerContentInfo.Companion.XCOROUTINE_POPUP_ACTION_GROUP
import org.jetbrains.kotlin.idea.debugger.coroutines.command.CoroutineStackFrameItem
import org.jetbrains.kotlin.idea.debugger.coroutines.command.CreationCoroutineStackFrameItem
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData
import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.CoroutinesDebugProbesProxy
import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.ManagerThreadExecutor
import org.jetbrains.kotlin.idea.debugger.coroutines.util.CreateContentParams
import org.jetbrains.kotlin.idea.debugger.coroutines.util.CreateContentParamsProvider
import org.jetbrains.kotlin.idea.debugger.coroutines.util.XDebugSessionListenerProvider
import org.jetbrains.kotlin.idea.debugger.coroutines.util.logger
import javax.swing.Icon
class XCoroutineView(val project: Project, val session: XDebugSession) :
Disposable, XDebugSessionListenerProvider, CreateContentParamsProvider {
val log by logger
val splitter = OnePixelSplitter("SomeKey", 0.25f)
val panel = XDebuggerTreePanel(project, session.debugProcess.editorsProvider, this, null, XCOROUTINE_POPUP_ACTION_GROUP, null)
val alarm = SingleAlarm(Runnable { clear() }, VIEW_CLEAR_DELAY, this)
val debugProcess: DebugProcessImpl = (session.debugProcess as JavaDebugProcess).debuggerSession.process
companion object {
private val VIEW_CLEAR_DELAY = 100 //ms
}
init {
splitter.firstComponent = panel.mainPanel
}
fun clear() {
DebuggerUIUtil.invokeLater {
panel.tree
.setRoot(object : XValueContainerNode<XValueContainer>(panel.tree, null, true, object : XValueContainer() {}) {}, false)
}
}
override fun dispose() {
}
fun forceClear() {
alarm.cancel()
clear()
}
fun createRoot(suspendContext: XSuspendContext) =
XCoroutinesRootNode(panel.tree, suspendContext)
override fun debugSessionListener(session: XDebugSession) =
CoroutineViewDebugSessionListener(session, this)
override fun createContentParams(): CreateContentParams =
CreateContentParams(
CoroutineDebuggerContentInfo.XCOROUTINE_THREADS_CONTENT,
splitter,
KotlinBundle.message("debugger.session.tab.xcoroutine.title"),
null,
panel.tree
)
}
class XCoroutinesRootNode(tree: XDebuggerTree, suspendContext: XSuspendContext) :
XValueContainerNode<CoroutineGroupContainer>(tree, null, false, CoroutineGroupContainer(suspendContext)) {
}
class CoroutineGroupContainer(
val suspendContext: XSuspendContext
) : XValueContainer() {
override fun computeChildren(node: XCompositeNode) {
val children = XValueChildrenList()
children.add("", CoroutineContainer(suspendContext as SuspendContextImpl, "Default group"))
node.addChildren(children, true)
}
}
class CoroutineContainer(
val suspendContext: SuspendContextImpl,
val groupName: String
) : XValue() {
override fun computeChildren(node: XCompositeNode) {
val managerThreadExecutor = ManagerThreadExecutor(suspendContext.debugProcess)
managerThreadExecutor.schedule {
val debugProbesProxy = CoroutinesDebugProbesProxy(suspendContext)
var coroutineCache = debugProbesProxy.dumpCoroutines()
if(coroutineCache.isOk()) {
val children = XValueChildrenList()
coroutineCache.cache.forEach {
children.add("", FramesContainer(it, debugProbesProxy, managerThreadExecutor))
}
node.addChildren(children, true)
} else
node.addChildren(XValueChildrenList.EMPTY, true)
}
}
override fun computePresentation(node: XValueNode, place: XValuePlace) {
node.setPresentation(AllIcons.Debugger.ThreadGroup, XRegularValuePresentation(groupName, null, ""), true)
}
}
class FramesContainer(
private val coroutineInfoData: CoroutineInfoData,
private val debugProbesProxy: CoroutinesDebugProbesProxy,
private val managerThreadExecutor: ManagerThreadExecutor
) : XValue() {
override fun computeChildren(node: XCompositeNode) {
managerThreadExecutor.schedule {
val children = XValueChildrenList()
debugProbesProxy.frameBuilder().build(coroutineInfoData)
val creationStack = mutableListOf<CreationCoroutineStackFrameItem>()
coroutineInfoData.stackFrameList.forEach {
val frameValue = when (it) {
is CreationCoroutineStackFrameItem -> {
creationStack.add(it)
null
}
else -> CoroutineFrameValue(it)
}
frameValue?.let {
children.add("", frameValue)
}
}
children.add("", CreationFramesContainer(creationStack))
node.addChildren(children, true)
}
}
override fun computePresentation(node: XValueNode, place: XValuePlace) {
val icon = when (coroutineInfoData.state) {
CoroutineInfoData.State.SUSPENDED -> AllIcons.Debugger.ThreadSuspended
CoroutineInfoData.State.RUNNING -> AllIcons.Debugger.ThreadRunning
CoroutineInfoData.State.CREATED -> AllIcons.Debugger.ThreadStates.Idle
}
val valuePresentation = customizePresentation(coroutineInfoData)
node.setPresentation(icon, valuePresentation, true)
}
private fun customizePresentation(coroutineInfoData: CoroutineInfoData): XRegularValuePresentation {
val component = SimpleColoredComponent()
val thread = coroutineInfoData.thread
val name = thread?.name()?.substringBefore(" @${coroutineInfoData.name}") ?: ""
val threadState = if (thread != null) DebuggerUtilsEx.getThreadStatusText(thread.status()) else ""
component.append("\"").append(coroutineInfoData.name, XDebuggerUIConstants.VALUE_NAME_ATTRIBUTES)
component.append("\": ${coroutineInfoData.state} ${if (name.isNotEmpty()) "on thread \"$name\":$threadState" else ""}")
return XRegularValuePresentation(component.getCharSequence(false).toString(), null, "")
}
}
class CreationFramesContainer(val creationFrames: List<CreationCoroutineStackFrameItem>) : XValue() {
override fun computeChildren(node: XCompositeNode) {
val children = XValueChildrenList()
creationFrames.forEach {
children.add("", CoroutineFrameValue(it))
}
node.addChildren(children, true)
}
override fun computePresentation(node: XValueNode, place: XValuePlace) {
node.setPresentation(AllIcons.Debugger.ThreadSuspended, XRegularValuePresentation("Creation stack frame", null, ""), true)
}
}
class CoroutineFrameValue(val frame: CoroutineStackFrameItem) : XValue() {
override fun computePresentation(node: XValueNode, place: XValuePlace) {
val presentation = customizePresentation(frame)
if(node is XValueNodeImpl) {
node.setPresentation(null, XRegularValuePresentation("", null, ""), false)
presentation.texts.forEachIndexed { i, s ->
node.text.append(s, presentation.attributes[i])
}
} else {
val component = SimpleColoredComponent()
presentation.appendToComponent(component)
val valuePresentation = XRegularValuePresentation(component.getCharSequence(false).toString(), null, "")
node.setPresentation(null, valuePresentation, false)
}
}
}
fun customizePresentation(frame: CoroutineStackFrameItem) : SimpleColoredText =
calcRepresentation(frame.location(), ThreadsViewSettings.getInstance())
/**
* Taken from #StackFrameDescriptorImpl.calcRepresentation
*/
fun calcRepresentation(location: Location, settings: ThreadsViewSettings): SimpleColoredText {
val label = SimpleColoredText()
DebuggerUIUtil.getColorScheme(null)
if (location.method() != null) {
val myName = location.method().name()
label.append(if (settings.SHOW_ARGUMENTS_TYPES) DebuggerUtilsEx.methodNameWithArguments(location.method()) else myName, XDebuggerUIConstants.VALUE_NAME_ATTRIBUTES)
}
if (settings.SHOW_LINE_NUMBER) {
label.append(":", SimpleTextAttributes.REGULAR_ATTRIBUTES)
label.append("" + DebuggerUtilsEx.getLineNumber(location, false), SimpleTextAttributes.REGULAR_ATTRIBUTES)
}
if (settings.SHOW_CLASS_NAME) {
val name: String?
name = try {
val refType: ReferenceType = location.declaringType()
refType?.name()
} catch (e: InternalError) {
e.toString()
}
if (name != null) {
label.append(", ", SimpleTextAttributes.REGULAR_ATTRIBUTES)
val dotIndex = name.lastIndexOf('.')
if (dotIndex < 0) {
label.append(name, SimpleTextAttributes.REGULAR_ATTRIBUTES)
} else {
label.append(name.substring(dotIndex + 1), SimpleTextAttributes.REGULAR_ATTRIBUTES)
if (settings.SHOW_PACKAGE_NAME) {
label.append(" (${name.substring( 0, dotIndex)})", SimpleTextAttributes.REGULAR_ATTRIBUTES)
}
}
}
}
if (settings.SHOW_SOURCE_NAME) {
label.append(", ", SimpleTextAttributes.REGULAR_ATTRIBUTES)
label.append(DebuggerUtilsEx.getSourceName(location) { e: Throwable? -> "Unknown Source" }, SimpleTextAttributes.REGULAR_ATTRIBUTES)
}
return label
}
@@ -222,6 +222,15 @@ class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDe
@Suppress("ConvertToStringTemplate")
return name().startsWith(AsmUtil.LABELED_THIS_PARAMETER)
}
override fun hashCode(): Int {
return super.hashCode()
}
override fun equals(other: Any?) : Boolean {
val eq = super.equals(other)
return other is KotlinStackFrame && eq
}
}
interface ThisLocalVariable {
@@ -10,7 +10,7 @@ import com.intellij.debugger.engine.JavaValue
import com.intellij.debugger.memory.utils.StackFrameItem
import com.intellij.execution.process.ProcessOutputTypes
import com.intellij.openapi.extensions.Extensions
import org.jetbrains.kotlin.idea.debugger.KotlinCoroutinesAsyncStackTraceProvider
import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineAsyncStackTraceProvider
import org.jetbrains.kotlin.idea.debugger.test.preference.DebuggerPreferences
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import org.jetbrains.kotlin.utils.getSafe
@@ -54,7 +54,7 @@ abstract class AbstractAsyncStackTraceTest : KotlinDescriptorTestCaseWithSteppin
}
}
private fun getAsyncStackTraceProvider(): KotlinCoroutinesAsyncStackTraceProvider? {
private fun getAsyncStackTraceProvider(): CoroutineAsyncStackTraceProvider? {
val area = Extensions.getArea(null)
if (!area.hasExtensionPoint(ASYNC_STACKTRACE_EP_NAME)) {
System.err.println("$ASYNC_STACKTRACE_EP_NAME extension point is not found (probably old IDE version)")
@@ -62,7 +62,7 @@ abstract class AbstractAsyncStackTraceTest : KotlinDescriptorTestCaseWithSteppin
}
val extensionPoint = area.getExtensionPoint<Any>(ASYNC_STACKTRACE_EP_NAME)
val provider = extensionPoint.extensions.firstIsInstanceOrNull<KotlinCoroutinesAsyncStackTraceProvider>()
val provider = extensionPoint.extensions.firstIsInstanceOrNull<CoroutineAsyncStackTraceProvider>()
if (provider == null) {
System.err.println("Kotlin coroutine async stack trace provider is not found")
@@ -5,59 +5,53 @@
package org.jetbrains.kotlin.idea.debugger.test
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.execution.configurations.JavaParameters
import com.intellij.execution.process.ProcessOutputTypes
import com.intellij.jarRepository.JarRepositoryManager
import com.intellij.jarRepository.RemoteRepositoryDescription
import org.jetbrains.idea.maven.aether.ArtifactKind
import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor
import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineState
import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutinesDebugProbesProxy
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData
import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.CoroutinesDebugProbesProxy
import org.jetbrains.kotlin.idea.debugger.test.preference.DebuggerPreferences
abstract class AbstractCoroutineDumpTest : KotlinDescriptorTestCaseWithStepping() {
override fun doMultiFileTest(files: TestFiles, preferences: DebuggerPreferences) {
doOnBreakpoint {
val evalContext = EvaluationContextImpl(this, frameProxy)
val execContext = ExecutionContext(evalContext, frameProxy ?: return@doOnBreakpoint)
val either = CoroutinesDebugProbesProxy.dumpCoroutines(execContext)
val infoCache = CoroutinesDebugProbesProxy(this).dumpCoroutines()
try {
if (either.isRight)
if (infoCache.isOk())
try {
val states = either.get()
val states = infoCache.cache
print(stringDump(states), ProcessOutputTypes.SYSTEM)
} catch (ignored: Throwable) {
}
else
throw AssertionError("Dump failed", either.left)
throw AssertionError("Dump failed")
} finally {
resume(this)
}
}
doOnBreakpoint {
val evalContext = EvaluationContextImpl(this, frameProxy)
val execContext = ExecutionContext(evalContext, frameProxy ?: return@doOnBreakpoint)
val either = CoroutinesDebugProbesProxy.dumpCoroutines(execContext)
val infoCache = CoroutinesDebugProbesProxy(this).dumpCoroutines()
try {
if (either.isRight)
if (infoCache.isOk())
try {
val states = either.get()
val states = infoCache.cache
print(stringDump(states), ProcessOutputTypes.SYSTEM)
} catch (ignored: Throwable) {
}
else
throw AssertionError("Dump failed", either.left)
throw AssertionError("Dump failed")
} finally {
resume(this)
}
}
}
private fun stringDump(states: List<CoroutineState>) = buildString {
states.forEach {
private fun stringDump(infoData: List<CoroutineInfoData>) = buildString {
infoData.forEach {
appendln("\"${it.name}\", state: ${it.state}")
}
}
@@ -18,6 +18,7 @@ import com.intellij.debugger.jdi.VirtualMachineProxyImpl
import com.intellij.openapi.project.Project
import com.sun.jdi.*
import com.sun.jdi.request.EventRequest
import org.jetbrains.kotlin.idea.debugger.hopelessAware
import org.jetbrains.org.objectweb.asm.Type
class ExecutionContext(val evaluationContext: EvaluationContextImpl, val frameProxy: StackFrameProxyImpl) {
@@ -100,4 +101,65 @@ class ExecutionContext(val evaluationContext: EvaluationContextImpl, val framePr
@Suppress("DEPRECATION")
DebuggerUtilsEx.keep(reference, evaluationContext)
}
}
fun findClassSafe(className: String): ClassType? =
hopelessAware { findClass(className) as? ClassType }
fun invokeMethodAsString(instance: ObjectReference, methodName: String): String? =
(findAndInvoke(instance, instance.referenceType(), methodName, "()Ljava/lang/String;") as? StringReference)?.value() ?: null
fun invokeMethodAsInt(instance: ObjectReference, methodName: String): Int? =
(findAndInvoke(instance, instance.referenceType(), methodName, "()I") as? IntegerValue)?.value() ?: null
fun invokeMethodAsObject(type: ClassType, methodName: String, vararg params: Value): ObjectReference? =
invokeMethodAsObject(type, methodName, null, *params)
fun invokeMethodAsObject(type: ClassType, methodName: String, methodSignature: String?, vararg params: Value): ObjectReference? =
findAndInvoke(type, methodName, methodSignature, *params) as? ObjectReference
fun invokeMethodAsObject(instance: ObjectReference, methodName: String, vararg params: Value): ObjectReference? =
invokeMethodAsObject(instance, methodName, null, *params)
fun invokeMethodAsObject(instance: ObjectReference, methodName: String, methodSignature: String?, vararg params: Value): ObjectReference? =
findAndInvoke(instance, methodName, methodSignature, *params) as? ObjectReference
fun invokeMethodAsObject(instance: ObjectReference, method: Method, vararg params: Value): ObjectReference? =
invokeMethod(instance, method, params.asList()) as? ObjectReference
fun invokeMethodAsVoid(type: ClassType, methodName: String, methodSignature: String? = null, vararg params: Value = emptyArray()) =
findAndInvoke(type, methodName, methodSignature, *params)
fun invokeMethodAsVoid(instance: ObjectReference, methodName: String, methodSignature: String? = null, vararg params: Value = emptyArray()) =
findAndInvoke(instance, methodName, methodSignature, *params)
fun invokeMethodAsArray(instance: ClassType, methodName: String, methodSignature: String, vararg params: Value): ArrayReference? =
findAndInvoke(instance, methodName, methodSignature, *params) as? ArrayReference
private fun findAndInvoke(ref: ObjectReference, type: ReferenceType, name: String, methodSignature: String, vararg params: Value): Value? {
val method = type.methodsByName(name, methodSignature).single()
return invokeMethod(ref, method, params.asList())
}
/**
* static method invocation
*/
fun findAndInvoke(type: ClassType, name: String, methodSignature: String? = null, vararg params: Value): Value? {
val method =
if (methodSignature is String)
type.methodsByName(name, methodSignature).single()
else
type.methodsByName(name).single()
return invokeMethod(type, method, params.asList())
}
fun findAndInvoke(instance: ObjectReference, name: String, methodSignature: String? = null, vararg params: Value): Value? {
val type = instance.referenceType()
val method =
if (methodSignature is String)
type.methodsByName(name, methodSignature).single()
else
type.methodsByName(name).single()
return invokeMethod(instance, method, params.asList())
}
}
+2 -2
View File
@@ -57,7 +57,7 @@
<group id="Kotlin.XDebugger.Actions">
<!-- TODO(design)-->
<action id="Kotlin.XDebugger.CoroutinesDump"
class="org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineDumpAction"
class="org.jetbrains.kotlin.idea.debugger.coroutines.command.CoroutineDumpAction"
text="Get Coroutines Dump"
icon="/org/jetbrains/kotlin/idea/icons/kotlin.png"/>
</group>
@@ -90,7 +90,7 @@
<applicationService serviceImplementation="org.jetbrains.kotlin.idea.debugger.ToggleKotlinVariablesState"/>
<debugger.asyncStackTraceProvider implementation="org.jetbrains.kotlin.idea.debugger.KotlinCoroutinesAsyncStackTraceProvider"/>
<debugger.asyncStackTraceProvider implementation="org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineAsyncStackTraceProvider"/>
<debugger.jvmSmartStepIntoHandler implementation="org.jetbrains.kotlin.idea.debugger.stepping.smartStepInto.KotlinSmartStepIntoHandler"/>
<debugger.positionManagerFactory implementation="org.jetbrains.kotlin.idea.debugger.KotlinPositionManagerFactory"/>
<debugger.codeFragmentFactory implementation="org.jetbrains.kotlin.idea.debugger.evaluate.KotlinCodeFragmentFactory"/>