(CoroutineDebugger) Alternative CoroutineInfoProvider added

Added way to retrieve coroutine information without an agent
in target JVM. 'kotlin.debugger.coroutines.switch' provides
two possibilities to test coroutines.
Agent way gets activated once kotlinx.coroutines.debug.DebugProbes
started with javaagent.
Library-less ways use DispatchedContinuation or ChildContinuationImpl
classes to retrieve coroutine information and stack traces.
This commit is contained in:
Vladimir Ilmov
2020-03-17 09:40:28 +01:00
parent ac6036f366
commit cd12226f20
48 changed files with 1902 additions and 1094 deletions
@@ -6,10 +6,10 @@
package org.jetbrains.kotlin.idea.configuration
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.util.text.StringUtil
import com.intellij.util.Consumer
import com.intellij.util.SystemProperties
import org.jetbrains.kotlin.idea.debugger.coroutine.standaloneCoroutineDebuggerEnabled
import org.jetbrains.plugins.gradle.service.project.AbstractProjectResolverExtension
class KotlinGradleCoroutineDebugProjectResolver : AbstractProjectResolverExtension() {
@@ -17,10 +17,9 @@ class KotlinGradleCoroutineDebugProjectResolver : AbstractProjectResolverExtensi
override fun enhanceTaskProcessing(taskNames: MutableList<String>, jvmParametersSetup: String?, initScriptConsumer: Consumer<String>) {
try {
if (coroutineDebuggerEnabled())
setupCoroutineAgentForJvmForkedTestTasks(initScriptConsumer)
setupCoroutineAgentForJvmForkedTestTasks(initScriptConsumer)
} catch (e: Exception) {
log.error("Gradle: not possible to attach coroutine debugger agent. Coroutine debugger disabled.", e)
log.error("Gradle: not possible to attach a coroutine debugger agent.", e)
}
}
@@ -37,7 +36,4 @@ class KotlinGradleCoroutineDebugProjectResolver : AbstractProjectResolverExtensi
val script = StringUtil.join(lines, SystemProperties.getLineSeparator())
initScriptConsumer.consume(script)
}
// supposed to be the same as [CoroutineProjectConnectionListener.kt].coroutineDebuggerEnabled
private fun coroutineDebuggerEnabled() = Registry.`is`("kotlin.debugger.coroutines")
}
@@ -22,4 +22,5 @@ coroutine.dump.threads.loading=Loading…
coroutine.view.title=Coroutines
coroutine.view.default.group=Default group
coroutine.view.fetching.error=An error occurred on fetching information
coroutine.view.fetching.error=An error occurred on fetching information
coroutine.view.fetching.not_found=No coroutine information found
@@ -5,47 +5,71 @@
package org.jetbrains.kotlin.idea.debugger.coroutine
import com.intellij.debugger.engine.*
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.engine.AsyncStackTraceProvider
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.xdebugger.frame.XSuspendContext
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.*
import com.intellij.debugger.memory.utils.StackFrameItem
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.*
import org.jetbrains.kotlin.idea.debugger.coroutine.data.PreCoroutineStackFrameItem
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineStackFrameItem
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.AsyncStackTraceContext
import org.jetbrains.kotlin.idea.debugger.evaluate.BaseExecutionContext
import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
import org.jetbrains.kotlin.idea.debugger.hopelessAware
import org.jetbrains.kotlin.idea.debugger.isInKotlinSources
class CoroutineAsyncStackTraceProvider : AsyncStackTraceProvider {
override fun getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: SuspendContextImpl): List<CoroutineStackFrameItem>? =
getAsyncStackTrace(stackFrame, suspendContext as XSuspendContext)
fun getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: XSuspendContext): List<CoroutineStackFrameItem>? {
val stackFrameList = hopelessAware { getAsyncStackTraceSafe(stackFrame.stackFrameProxy, suspendContext) }
return if (stackFrameList == null || stackFrameList.isEmpty()) null else stackFrameList
override fun getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: SuspendContextImpl): List<StackFrameItem>? {
val stackFrameList = hopelessAware {
if (stackFrame is CoroutinePreflightStackFrame)
lookupForAfterPreflight(stackFrame, suspendContext)
else
null
}
return if (stackFrameList == null || stackFrameList.isEmpty())
null
else stackFrameList
}
fun getAsyncStackTraceSafe(frameProxy: StackFrameProxyImpl, suspendContext: XSuspendContext): List<CoroutineStackFrameItem> {
val defaultResult = emptyList<CoroutineStackFrameItem>()
fun lookupForAfterPreflight(
stackFrame: CoroutinePreflightStackFrame,
suspendContext: SuspendContextImpl
): List<CoroutineStackFrameItem>? {
val resumeWithFrame = stackFrame.resumeWithFrame
if (threadAndContextSupportsEvaluation(suspendContext, resumeWithFrame)) {
val stackFrames = mutableListOf<CoroutineStackFrameItem>()
stackFrames.addAll(
stackFrame.restoredStackFrame.drop(1).dropLast(1)
) // because first frame has been generated via CoroutinePreflightStackFrame
val lastRestoredFrame = stackFrame.restoredStackFrame.last()
stackFrames.addAll(stackFrame.threadPreCoroutineFrames.mapIndexed { index, stackFrameProxyImpl ->
if (index == 0)
PreCoroutineStackFrameItem(stackFrameProxyImpl, lastRestoredFrame) // get location and variables also from restored part
else
PreCoroutineStackFrameItem(stackFrameProxyImpl)
})
return stackFrames
}
return null
}
fun lookupForResumeContinuation(
frameProxy: StackFrameProxyImpl,
suspendContext: SuspendContextImpl
): List<CoroutineStackFrameItem>? {
val location = frameProxy.location()
if (!location.isInKotlinSources())
return defaultResult
return null
val method = location.safeMethod() ?: return defaultResult
val threadReference = frameProxy.threadProxy().threadReference
if (threadReference == null || !threadReference.isSuspended || !canRunEvaluation(suspendContext))
return defaultResult
val context = DefaultExecutionContext(suspendContext as SuspendContextImpl, frameProxy)
val astContext = AsyncStackTraceContext(context, method)
return astContext.getAsyncStackTraceIfAny()
if (threadAndContextSupportsEvaluation(suspendContext, frameProxy))
return ContinuationHolder.lookupForResumeMethodContinuation(suspendContext, frameProxy)
?.getAsyncStackTraceIfAny()?.stackFrameItems
return null
}
fun canRunEvaluation(suspendContext: XSuspendContext) =
(suspendContext as SuspendContextImpl).debugProcess.canRunEvaluation
private fun threadAndContextSupportsEvaluation(suspendContext: SuspendContextImpl, frameProxy: StackFrameProxyImpl) =
suspendContext.supportsEvaluation() && frameProxy.threadProxy().supportsEvaluation()
}
@@ -0,0 +1,37 @@
/*
* 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.coroutine;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.RunConfigurationExtension;
import com.intellij.execution.configurations.JavaParameters;
import com.intellij.execution.configurations.RunConfigurationBase;
import com.intellij.execution.configurations.RunnerSettings;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
public class CoroutineDebugConfigurationExtension extends RunConfigurationExtension {
private static final Logger log = Logger.getInstance(CoroutineDebugConfigurationExtension.class);
@Override
public <T extends RunConfigurationBase> void updateJavaParameters(
T configuration, JavaParameters params, RunnerSettings runnerSettings
) throws ExecutionException {
if (configuration != null) {
Project project = configuration.getProject();
DebuggerListener listener = ServiceManager.getService(project, DebuggerListener.class);
if (listener != null)
listener.registerDebuggerConnection(configuration, params, runnerSettings);
else
log.error("DebuggerListener service is not found in project " + project.getName());
}
}
@Override
public boolean isApplicableFor(RunConfigurationBase<?> configuration) {
return true;
}
}
@@ -1,30 +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.coroutine
import com.intellij.execution.RunConfigurationExtension
import com.intellij.execution.configurations.DebuggingRunnerData
import com.intellij.execution.configurations.JavaParameters
import com.intellij.execution.configurations.RunConfigurationBase
import com.intellij.execution.configurations.RunnerSettings
/**
* Installs coroutines debug agent and coroutines tab if `kotlinx-coroutines-debug` dependency is found
*/
@Suppress("IncompatibleAPI")
class CoroutineDebugConfigurationExtension : RunConfigurationExtension() {
override fun isApplicableFor(configuration: RunConfigurationBase<*>) = coroutineDebuggerEnabled()
override fun <T : RunConfigurationBase<*>?> updateJavaParameters(
configuration: T,
params: JavaParameters,
runnerSettings: RunnerSettings?
) {
if (runnerSettings is DebuggingRunnerData && configuration is RunConfigurationBase<*>) {
configuration.project.coroutineConnectionListener.configurationStarting(configuration, params, runnerSettings)
}
}
}
@@ -1,30 +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.coroutine
import com.intellij.execution.RunConfigurationExtension
import com.intellij.execution.configurations.DebuggingRunnerData
import com.intellij.execution.configurations.JavaParameters
import com.intellij.execution.configurations.RunConfigurationBase
import com.intellij.execution.configurations.RunnerSettings
/**
* Installs coroutines debug agent and coroutines tab if `kotlinx-coroutines-debug` dependency is found
*/
@Suppress("IncompatibleAPI")
class CoroutineDebugConfigurationExtension : RunConfigurationExtension() {
override fun isApplicableFor(configuration: RunConfigurationBase<*>) = coroutineDebuggerEnabled()
override fun <T : RunConfigurationBase<*>?> updateJavaParameters(
configuration: T,
params: JavaParameters?,
runnerSettings: RunnerSettings?
) {
if (runnerSettings is DebuggingRunnerData && configuration is RunConfigurationBase<*>) {
configuration.project.coroutineConnectionListener.configurationStarting(configuration, params, runnerSettings)
}
}
}
@@ -13,10 +13,3 @@ class CoroutineDebuggerContentInfo {
val XCOROUTINE_POPUP_ACTION_GROUP = "Kotlin.XDebugger.Actions"
}
}
class CoroutineDebuggerActions {
companion object {
@NonNls
val COROUTINE_PANEL_POPUP: String = "Debugger.CoroutinesPanelPopup"
}
}
@@ -1,117 +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.coroutine
import com.intellij.debugger.DebuggerInvocationUtil
import com.intellij.debugger.engine.JavaDebugProcess
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.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
import com.intellij.ui.content.Content
import com.intellij.util.messages.MessageBusConnection
import com.intellij.xdebugger.*
import com.intellij.xdebugger.impl.XDebugSessionImpl
import org.jetbrains.kotlin.idea.debugger.coroutine.util.*
import org.jetbrains.kotlin.idea.debugger.coroutine.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
private 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?
) {
if (coroutineDebuggerEnabled()) {
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" == configurationName || "KotlinGradleRunConfiguration" == configurationName
override fun processStarted(debugProcess: XDebugProcess) =
DebuggerInvocationUtil.swingInvokeLater(project) {
if (debugProcess is JavaDebugProcess) {
registerXCoroutinesPanel(debugProcess.session)
}
}
override fun processStopped(debugProcess: XDebugProcess) {
if (processCounter.compareAndSet(1, 0)) {
connection?.disconnect()
connection = null
} else
processCounter.decrementAndGet()
}
private fun registerXCoroutinesPanel(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)
}
}
val Project.coroutineConnectionListener by projectListener
val projectListener
get() = object : ReadOnlyProperty<Project, CoroutineProjectConnectionListener> {
lateinit var listenerProject: CoroutineProjectConnectionListener
override fun getValue(thisRef: Project, property: KProperty<*>): CoroutineProjectConnectionListener {
if (!::listenerProject.isInitialized)
listenerProject = CoroutineProjectConnectionListener(thisRef)
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")
}
@@ -14,8 +14,12 @@ import com.intellij.xdebugger.frame.XStackFrame
import com.intellij.xdebugger.impl.XDebugSessionImpl
import com.sun.jdi.Location
import org.jetbrains.kotlin.idea.debugger.StackFrameInterceptor
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.ContinuationHolder
class CoroutineStackFrameInterceptor(val project: Project) : StackFrameInterceptor {
override fun createStackFrame(frame: StackFrameProxyImpl, debugProcess: DebugProcessImpl, location: Location): XStackFrame? =
null
if (AsyncStacksToggleAction.isAsyncStacksEnabled(debugProcess.xdebugProcess?.session as XDebugSessionImpl))
ContinuationHolder.coroutineExitFrame(frame, debugProcess.debuggerContext.suspendContext as SuspendContextImpl)
else
null
}
@@ -0,0 +1,105 @@
/*
* Copyright 2010-2020 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.coroutine
import com.intellij.debugger.DebuggerInvocationUtil
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.JavaDebugProcess
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.impl.PrioritizedTask
import com.intellij.execution.configurations.DebuggingRunnerData
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.openapi.Disposable
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.registry.Registry
import com.intellij.ui.content.Content
import com.intellij.util.messages.MessageBusConnection
import com.intellij.xdebugger.XDebugProcess
import com.intellij.xdebugger.XDebugSession
import com.intellij.xdebugger.XDebuggerManager
import com.intellij.xdebugger.XDebuggerManagerListener
import com.intellij.xdebugger.impl.XDebugSessionImpl
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.ManagerThreadExecutor
import org.jetbrains.kotlin.idea.debugger.coroutine.util.CreateContentParamsProvider
import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger
import org.jetbrains.kotlin.idea.debugger.coroutine.view.XCoroutineView
class DebuggerConnection(
val project: Project,
val configuration: RunConfigurationBase<*>,
val params: JavaParameters?,
val runnerSettings: DebuggingRunnerData?
) : XDebuggerManagerListener {
var disposable: Disposable? = null
var connection: MessageBusConnection? = null
private val log by logger
init {
if (params is JavaParameters) {
// gradle related logic in KotlinGradleCoroutineDebugProjectResolver
val kotlinxCoroutinesClassPathLib =
params.classPath?.pathList?.firstOrNull { it.contains("kotlinx-coroutines-debug") }
if (kotlinxCoroutinesClassPathLib is String)
initializeCoroutineAgent(params, kotlinxCoroutinesClassPathLib)
else
log.warn("'kotlinx-coroutines-debug' not found in classpath.")
}
connect()
}
private fun initializeCoroutineAgent(params: JavaParameters, it: String?) {
params.vmParametersList?.add("-javaagent:$it")
params.vmParametersList?.add("-ea")
}
private fun connect() {
connection = project.messageBus.connect()
connection?.subscribe(XDebuggerManager.TOPIC, this)
}
override fun processStarted(debugProcess: XDebugProcess) =
DebuggerInvocationUtil.swingInvokeLater(project) {
if (debugProcess is JavaDebugProcess) {
disposable = registerXCoroutinesPanel(debugProcess.session)
}
}
override fun processStopped(debugProcess: XDebugProcess) {
val rootDisposable = disposable
if (rootDisposable is Disposable && debugProcess is JavaDebugProcess && debugProcess.session.suspendContext is SuspendContextImpl) {
ManagerThreadExecutor(debugProcess).on(debugProcess.session.suspendContext).schedule {
Disposer.dispose(rootDisposable)
disposable = null
}
}
connection?.disconnect()
connection = null
}
private fun registerXCoroutinesPanel(session: XDebugSession): Disposable? {
val ui = session.ui ?: return null
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()
return xCoroutineThreadView
}
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)
}
}
fun standaloneCoroutineDebuggerEnabled() = Registry.`is`("kotlin.debugger.coroutines.standalone")
fun coroutineDebuggerTraceEnabled() = Registry.`is`("kotlin.debugger.coroutines.trace")
@@ -0,0 +1,46 @@
/*
* Copyright 2010-2020 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.coroutine
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.externalSystem.service.execution.ExternalSystemRunConfiguration
import com.intellij.openapi.project.Project
import com.intellij.xdebugger.XDebuggerManagerListener
import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger
interface DebuggerListener : XDebuggerManagerListener {
fun registerDebuggerConnection(
configuration: RunConfigurationBase<*>,
params: JavaParameters?,
runnerSettings: RunnerSettings?
): DebuggerConnection?
}
class CoroutineDebuggerListener(val project: Project) : DebuggerListener {
val log by logger
override fun registerDebuggerConnection(
configuration: RunConfigurationBase<*>,
params: JavaParameters?,
runnerSettings: RunnerSettings?
): DebuggerConnection? {
val isExternalSystemRunConfiguration = configuration is ExternalSystemRunConfiguration
val isGradleConfiguration = gradleConfiguration(configuration.type.id)
if (runnerSettings == null || isExternalSystemRunConfiguration || isGradleConfiguration) {
log.warn("Coroutine debugger in standalone mode for ${configuration.name} ${configuration.javaClass} / ${params?.javaClass} / ${runnerSettings?.javaClass} (if enabled)")
} else if (runnerSettings is DebuggingRunnerData?)
return DebuggerConnection(project, configuration, params, runnerSettings)
return null
}
private fun gradleConfiguration(configurationName: String) =
"GradleRunConfiguration" == configurationName || "KotlinGradleRunConfiguration" == configurationName
}
@@ -5,8 +5,8 @@
package org.jetbrains.kotlin.idea.debugger.coroutine
import com.intellij.ui.components.JBLabel
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.ui.components.JBLabel
import javax.swing.ListCellRenderer
class VersionedImplementationProvider {
@@ -13,162 +13,68 @@ import com.intellij.debugger.jdi.GeneratedLocation
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl
import com.intellij.util.containers.ContainerUtil
import com.intellij.xdebugger.frame.XStackFrame
import com.intellij.xdebugger.frame.XSuspendContext
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.coroutine.CoroutineAsyncStackTraceProvider
import org.jetbrains.kotlin.idea.debugger.coroutine.data.*
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.isPreFlight
import org.jetbrains.kotlin.idea.debugger.safeLineNumber
import org.jetbrains.kotlin.idea.debugger.safeLocation
import org.jetbrains.kotlin.idea.debugger.safeMethod
class CoroutineBuilder(val suspendContext: XSuspendContext) {
class CoroutineBuilder(val suspendContext: SuspendContextImpl) {
private val coroutineStackFrameProvider = CoroutineAsyncStackTraceProvider()
val debugProcess = (suspendContext as SuspendContextImpl).debugProcess
val debugProcess = suspendContext.debugProcess
private val virtualMachineProxy = debugProcess.virtualMachineProxy
private val classesByName = ClassesByNameProvider.createCache(virtualMachineProxy.allClasses())
companion object {
const val CREATION_STACK_TRACE_SEPARATOR = "\b\b\b" // the "\b\b\b" is used for creation stacktrace separator in kotlinx.coroutines
const val CREATION_STACK_TRACE_SEPARATOR = "\b\b\b" // the "\b\b\b" is used as creation stacktrace separator in kotlinx.coroutines
}
fun build(coroutine: CoroutineInfoData): List<CoroutineStackFrameItem> {
val coroutineStackFrameList = mutableListOf<CoroutineStackFrameItem>()
val firstSuspendedStackFrameProxyImpl = firstSuspendedThreadFrame()
val creationFrameSeparatorIndex = findCreationFrameIndex(coroutine.stackTrace)
if (coroutine.state == CoroutineInfoData.State.RUNNING && coroutine.activeThread is ThreadReference) {
val threadReferenceProxyImpl = runningThreadProxy(coroutine.activeThread)
val executionStack = JavaExecutionStack(threadReferenceProxyImpl, debugProcess, suspendedSameThread(coroutine.activeThread))
if (coroutine.isRunning() && coroutine.activeThread is ThreadReference) {
val threadReferenceProxyImpl = ThreadReferenceProxyImpl(debugProcess.virtualMachineProxy, coroutine.activeThread)
val frames = threadReferenceProxyImpl.forceFrames()
val realFrames = threadReferenceProxyImpl.forceFrames()
var coroutineStackInserted = false
for (runningStackFrameProxy in frames) {
val jStackFrame = executionStack.createStackFrame(runningStackFrameProxy)
val coroutineStack = coroutineStackFrameProvider.getAsyncStackTraceSafe(runningStackFrameProxy, suspendContext)
if (coroutineStack.isNotEmpty()) {
// clue coroutine stack into the thread's real stack
val firstMergedFrame = mergeFrameVars(coroutineStack.first(), runningStackFrameProxy, jStackFrame)
coroutineStackFrameList.add(firstMergedFrame)
for (asyncFrame in coroutineStack.drop(1)) {
coroutineStackFrameList.add(
RestoredCoroutineStackFrameItem(
runningStackFrameProxy,
asyncFrame.location,
asyncFrame.spilledVariables
)
)
coroutineStackInserted = true
}
} else {
if (coroutineStackInserted && isInvokeSuspendNegativeLineMethodFrame(runningStackFrameProxy)) {
coroutineStackInserted = false
} else
coroutineStackFrameList.add(RunningCoroutineStackFrameItem(runningStackFrameProxy, jStackFrame))
var preflightFound = false
for (runningStackFrameProxy in realFrames) {
if (runningStackFrameProxy.location().isPreFlight()) {
preflightFound = true
continue
}
}
} else if ((coroutine.state == CoroutineInfoData.State
.SUSPENDED || coroutine.activeThread == null) && coroutine.lastObservedFrameFieldRef is ObjectReference
) {
// to get frames from CoroutineInfo anyway
// the thread is paused on breakpoint - it has at least one frame
val suspendedStackTrace = coroutine.stackTrace.take(creationFrameSeparatorIndex)
for (suspendedFrame in suspendedStackTrace) {
val location = createLocation(suspendedFrame)
coroutineStackFrameList.add(
SuspendCoroutineStackFrameItem(
firstSuspendedStackFrameProxyImpl,
suspendedFrame,
coroutine.lastObservedFrameFieldRef,
location
)
)
}
}
if (preflightFound) {
val coroutineStack = coroutineStackFrameProvider.lookupForResumeContinuation(runningStackFrameProxy, suspendContext)
if (coroutineStack?.isNotEmpty() == true) {
// clue coroutine stack into the thread's real stack
coroutine.stackTrace.subList(creationFrameSeparatorIndex + 1, coroutine.stackTrace.size).forEach {
val location = createLocation(it)
coroutineStackFrameList.add(CreationCoroutineStackFrameItem(firstSuspendedStackFrameProxyImpl, it, location))
}
for (asyncFrame in coroutineStack) {
coroutineStackFrameList.add(
RestoredCoroutineStackFrameItem(
runningStackFrameProxy,
asyncFrame.location,
asyncFrame.spilledVariables
)
)
coroutineStackInserted = true
}
}
preflightFound = false
}
if (!(coroutineStackInserted && isInvokeSuspendNegativeLineMethodFrame(runningStackFrameProxy)))
coroutineStackFrameList.add(RunningCoroutineStackFrameItem(runningStackFrameProxy))
coroutineStackInserted = false
}
} else if ((coroutine.isSuspended() || coroutine.activeThread == null) && coroutine.lastObservedFrameFieldRef is ObjectReference)
coroutineStackFrameList.addAll(coroutine.stackTrace)
coroutineStackFrameList.addAll(coroutine.creationStackTrace)
coroutine.stackFrameList.addAll(coroutineStackFrameList)
return coroutineStackFrameList
}
/**
* First frames need to be merged as real frame has accurate line number but lacks local variables from coroutine-restored frame.
*/
private fun mergeFrameVars(
restoredFrame: CoroutineStackFrameItem,
runningStackFrameProxy: StackFrameProxyImpl,
jStackFrame: XStackFrame,
): RunningCoroutineStackFrameItem {
if (restoredFrame.location is GeneratedLocation) {
val restoredMethod = restoredFrame.location.method()
val realMethod = runningStackFrameProxy.location().method()
// if refers to the same method - proceed with merge, otherwise do nothing
if (restoredMethod == realMethod) {
return RunningCoroutineStackFrameItem(runningStackFrameProxy, jStackFrame, restoredFrame.spilledVariables)
}
}
return RunningCoroutineStackFrameItem(runningStackFrameProxy, jStackFrame)
}
private fun suspendedSameThread(activeThread: ThreadReference) =
activeThread == suspendedThreadProxy().threadReference
private fun createLocation(stackTraceElement: StackTraceElement): Location = 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(debugProcess, type, methodName, line)
}
/**
* Tries to find creation frame separator if any, returns last index if none found
*/
private fun findCreationFrameIndex(frames: List<StackTraceElement>): Int {
val 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 coroutine thread proxy
fun runningThreadProxy(threadReference: ThreadReference) =
ThreadReferenceProxyImpl(debugProcess.virtualMachineProxy, threadReference)
// retrieves current suspended thread proxy
private fun suspendedThreadProxy(): ThreadReferenceProxyImpl =
(suspendContext as SuspendContextImpl).thread!! // @TODO hash replace !!
private fun isInvokeSuspendNegativeLineMethodFrame(frame: StackFrameProxyImpl) =
frame.safeLocation()?.safeMethod()?.name() == "invokeSuspend" &&
frame.safeLocation()?.safeMethod()?.signature() == "(Ljava/lang/Object;)Ljava/lang/Object;" &&
@@ -25,7 +25,6 @@ import com.intellij.util.text.DateFormatUtil
import com.intellij.xdebugger.impl.XDebuggerManagerImpl
import org.jetbrains.kotlin.idea.debugger.coroutine.KotlinDebuggerCoroutinesBundle
import org.jetbrains.kotlin.idea.debugger.coroutine.view.CoroutineDumpPanel
import org.jetbrains.kotlin.idea.debugger.coroutine.coroutineDebuggerEnabled
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.CoroutineDebugProbesProxy
@@ -43,14 +42,14 @@ class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate {
val states = CoroutineDebugProbesProxy(context.suspendContext ?: return)
.dumpCoroutines()
if (states.isOk()) {
val message = KotlinDebuggerCoroutinesBundle.message("coroutine.dump.failed")
XDebuggerManagerImpl.NOTIFICATION_GROUP.createNotification(message,MessageType.ERROR).notify(project)
} else {
val f = fun() {
val ui = session.xDebugSession?.ui ?: return
addCoroutineDump(project, states.cache, ui, session.searchScope)
}
ApplicationManager.getApplication().invokeLater(f, ModalityState.NON_MODAL)
} else {
val message = KotlinDebuggerCoroutinesBundle.message("coroutine.dump.failed")
XDebuggerManagerImpl.NOTIFICATION_GROUP.createNotification(message,MessageType.ERROR).notify(project)
}
}
})
@@ -93,7 +92,7 @@ class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate {
return
}
val debuggerSession = DebuggerManagerEx.getInstanceEx(project).context.debuggerSession
presentation.isEnabled = debuggerSession != null && debuggerSession.isAttached && coroutineDebuggerEnabled()
presentation.isEnabled = debuggerSession != null && debuggerSession.isAttached
presentation.isVisible = presentation.isEnabled
}
}
@@ -0,0 +1,31 @@
/*
* Copyright 2010-2020 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.coroutine.data
import com.intellij.debugger.DebuggerContext
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.ui.impl.watch.ValueDescriptorImpl
import com.intellij.openapi.project.Project
import com.sun.jdi.Value
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.ContinuationHolder
class ContinuationValueDescriptorImpl(
project: Project,
val continuation: ContinuationHolder,
val fieldName: String,
val variableName: String
) : ValueDescriptorImpl(project) {
override fun calcValueName() = variableName
override fun calcValue(evaluationContext: EvaluationContextImpl?): Value? {
val field = continuation.referenceType()?.fieldByName(fieldName) ?: return null
return continuation.field(field)
}
override fun getDescriptorEvaluation(context: DebuggerContext?) =
throw EvaluateException("Spilled variable evaluation is not supported")
}
@@ -1,48 +1,55 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Copyright 2010-2020 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.coroutine.data
import com.intellij.debugger.engine.DebugProcessImpl
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.StackFrameDescriptorImpl
import com.intellij.ui.ColoredTextContainer
import com.intellij.ui.SimpleTextAttributes
import com.intellij.xdebugger.frame.XCompositeNode
import com.intellij.xdebugger.frame.XNamedValue
import com.intellij.xdebugger.frame.XStackFrame
import com.sun.jdi.Location
import com.sun.jdi.ObjectReference
import org.jetbrains.kotlin.idea.debugger.*
import org.jetbrains.kotlin.idea.debugger.coroutine.util.EmptyStackFrameDescriptor
import org.jetbrains.kotlin.idea.debugger.coroutine.coroutineDebuggerTraceEnabled
import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger
class CreationCoroutineStackFrameItem(
val frame: StackFrameProxyImpl,
val stackTraceElement: StackTraceElement,
location: Location
) : CoroutineStackFrameItem(location, emptyList()) {
fun emptyDescriptor() =
fun emptyDescriptor(frame: StackFrameProxyImpl) =
EmptyStackFrameDescriptor(stackTraceElement, frame)
}
/**
* Suspended frames in Suspended coroutine
*/
class SuspendCoroutineStackFrameItem(
val frame: StackFrameProxyImpl,
val stackTraceElement: StackTraceElement,
val lastObservedFrameFieldRef: ObjectReference,
location: Location,
spilledVariables: List<XNamedValue> = emptyList()
) : CoroutineStackFrameItem(location, spilledVariables) {
fun emptyDescriptor() =
fun emptyDescriptor(frame: StackFrameProxyImpl) =
EmptyStackFrameDescriptor(stackTraceElement, frame)
}
class RunningCoroutineStackFrameItem(
val frame: StackFrameProxyImpl,
val stackFrame: XStackFrame,
// val stackFrame: XStackFrame,
spilledVariables: List<XNamedValue> = emptyList()
) : CoroutineStackFrameItem(frame.location(), spilledVariables)
/**
* Restored frame in Running coroutine, attaching to running thread
*/
class RestoredCoroutineStackFrameItem(
val frame: StackFrameProxyImpl,
location: Location,
@@ -52,8 +59,71 @@ class RestoredCoroutineStackFrameItem(
StackFrameDescriptorImpl(frame, MethodsTracker())
}
/**
* Restored from memory dump
*/
class DefaultCoroutineStackFrameItem(location: Location, spilledVariables: List<XNamedValue>) :
CoroutineStackFrameItem(location, spilledVariables)
CoroutineStackFrameItem(location, spilledVariables) {
fun emptyDescriptor(frame: StackFrameProxyImpl) =
StackFrameDescriptorImpl(frame, MethodsTracker())
}
/**
* Original frame appeared before resumeWith call.
*
* Sequence is the following
*
* - KotlinStackFrame
* - invokeSuspend(KotlinStackFrame) -|
* | replaced with CoroutinePreflightStackFrame
* - resumeWith(KotlinStackFrame) ----|
* - PreCoroutineStackFrameItem part of CoroutinePreflightStackFrame
*
*/
class PreCoroutineStackFrameItem(val frame: StackFrameProxyImpl, location: Location, variables: List<XNamedValue> = emptyList()) :
CoroutineStackFrameItem(location, variables) {
constructor(frame: StackFrameProxyImpl, variables: List<XNamedValue> = emptyList()) : this(frame, frame.location(), variables)
constructor(frame: StackFrameProxyImpl, restoredCoroutineStackFrameItem: CoroutineStackFrameItem) : this(
frame,
restoredCoroutineStackFrameItem.location,
restoredCoroutineStackFrameItem.spilledVariables
)
override fun createFrame(debugProcess: DebugProcessImpl): CapturedStackFrame {
return PreCoroutineStackFrame(frame, debugProcess, this)
}
}
/**
* Can act as a joint frame, take variables form restored frame and information from the original one.
*/
class PreCoroutineStackFrame(val frame: StackFrameProxyImpl, val debugProcess: DebugProcessImpl, item: StackFrameItem) :
CoroutineStackFrame(debugProcess, item) {
override fun computeChildren(node: XCompositeNode) {
debugProcess.invokeInManagerThread {
debugProcess.getPositionManager().createStackFrame(frame, debugProcess, frame.location())
?.computeChildren(node) // hack but works
}
super.computeChildren(node)
}
}
open class CoroutineStackFrame(debugProcess: DebugProcessImpl, val item: StackFrameItem) :
StackFrameItem.CapturedStackFrame(debugProcess, item) {
override fun customizePresentation(component: ColoredTextContainer) {
if (coroutineDebuggerTraceEnabled())
component.append("${item.javaClass.simpleName} / ${this.javaClass.simpleName}", SimpleTextAttributes.GRAYED_ATTRIBUTES)
super.customizePresentation(component)
}
override fun getCaptionAboveOf() = "CoroutineExit"
override fun hasSeparatorAbove(): Boolean =
false
}
sealed class CoroutineStackFrameItem(val location: Location, val spilledVariables: List<XNamedValue>) :
StackFrameItem(location, spilledVariables) {
@@ -63,4 +133,11 @@ sealed class CoroutineStackFrameItem(val location: Location, val spilledVariable
return location.safeSourceName() + ":" + location.safeMethod().toString() + ":" +
location.safeLineNumber() + ":" + location.safeKotlinPreferredLineNumber()
}
}
override fun createFrame(debugProcess: DebugProcessImpl): CapturedStackFrame {
return CoroutineStackFrame(debugProcess, this)
}
}
class EmptyStackFrameDescriptor(val frame: StackTraceElement, proxy: StackFrameProxyImpl) :
StackFrameDescriptorImpl(proxy, MethodsTracker())
@@ -14,9 +14,10 @@ class CoroutineInfoCache(
state = CacheState.OK
}
fun fail() {
fun fail(): CoroutineInfoCache {
cache.clear()
state = CacheState.FAIL
return this
}
fun isOk(): Boolean {
@@ -7,17 +7,16 @@ package org.jetbrains.kotlin.idea.debugger.coroutine.data
import com.sun.jdi.ObjectReference
import com.sun.jdi.ThreadReference
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.CoroutineHolder
/**
* Represents state of a coroutine.
* @see `kotlinx.coroutines.debug.CoroutineInfo`
*/
data class CoroutineInfoData(
val name: String,
val state: State,
val stackTrace: List<StackTraceElement>,
// links to jdi.* references
val key: CoroutineNameIdState,
val stackTrace: List<CoroutineStackFrameItem>,
val creationStackTrace: List<CreationCoroutineStackFrameItem>,
val activeThread: ThreadReference? = null, // for suspended coroutines should be null
val lastObservedFrameFieldRef: ObjectReference?
) {
@@ -26,22 +25,41 @@ data class CoroutineInfoData(
// @TODO for refactoring/removal along with DumpPanel
val stringStackTrace: String by lazy {
buildString {
appendln("\"$name\", state: $state")
appendln("\"${key.name}\", state: ${key.state}")
stackTrace.forEach {
appendln("\t$it")
}
}
}
fun isSuspended() = state == State.SUSPENDED
fun isSuspended() = key.state == State.SUSPENDED
fun isCreated() = state == State.CREATED
fun isCreated() = key.state == State.CREATED
fun isEmptyStackTrace() = stackTrace.isEmpty()
fun isEmptyStack() = stackTrace.isEmpty()
enum class State {
RUNNING,
SUSPENDED,
CREATED
fun isRunning() = key.state == State.RUNNING
companion object {
fun suspendedCoroutineInfoData(
holder: CoroutineHolder,
lastObservedFrameFieldRef: ObjectReference
): CoroutineInfoData? {
return CoroutineInfoData(holder.info, holder.stackFrameItems, emptyList(), null, lastObservedFrameFieldRef)
}
}
}
data class CoroutineNameIdState(val name: String, val id: String, val state: State)
enum class State {
RUNNING,
SUSPENDED,
CREATED,
UNKNOWN,
SUSPENDED_COMPLETING,
SUSPENDED_CANCELLING,
CANCELLED,
COMPLETED,
NEW
}
@@ -24,18 +24,18 @@ import javax.swing.Icon
class CoroutineDescriptorImpl(val infoData: CoroutineInfoData) : NodeDescriptorImpl() {
lateinit var icon: Icon
override fun getName() = infoData.name
override fun getName() = infoData.key.name
@Throws(EvaluateException::class)
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener): String {
val thread = infoData.activeThread
val name = thread?.name()?.substringBefore(" @${infoData.name}") ?: ""
val name = thread?.name()?.substringBefore(" @${infoData.key.name}") ?: ""
val threadState = if (thread != null) DebuggerUtilsEx.getThreadStatusText(thread.status()) else ""
val threadName = if (name.isNotEmpty()) " on thread \"$name\":$threadState" else ""
return "${infoData.name}: ${infoData.state} $threadName"
return "${infoData.key.name}: ${infoData.key.state} $threadName"
}
override fun isExpandable() = infoData.state != CoroutineInfoData.State.CREATED
override fun isExpandable() = infoData.key.state != State.CREATED
private fun calcIcon() = when {
infoData.isSuspended() -> AllIcons.Debugger.ThreadSuspended
@@ -1,174 +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.coroutine.proxy
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.coroutine.util.logger
import org.jetbrains.kotlin.idea.debugger.SUSPEND_LAMBDA_CLASSES
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineStackFrameItem
import org.jetbrains.kotlin.idea.debugger.coroutine.data.DefaultCoroutineStackFrameItem
import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext
import org.jetbrains.kotlin.idea.debugger.isSubtype
import org.jetbrains.kotlin.idea.debugger.safeVisibleVariableByName
class AsyncStackTraceContext(
val context: DefaultExecutionContext,
val method: Method
) {
val log by logger
private companion object {
const val DEBUG_METADATA_KT = "kotlin.coroutines.jvm.internal.DebugMetadataKt"
}
fun getAsyncStackTraceIfAny(): List<CoroutineStackFrameItem> {
val continuation = locateContinuation() ?: return emptyList()
val frames = mutableListOf<CoroutineStackFrameItem>()
try {
collectFramesRecursively(continuation, frames)
} catch (e: Exception) {
log.error("Error while looking for variables.", e)
}
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 continuationVariable = context.frameProxy?.safeVisibleVariableByName(CONTINUATION_VARIABLE_NAME) ?: return null
continuation = context.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<CoroutineStackFrameItem>) {
val continuationType = continuation.referenceType() as? ClassType ?: return
val baseContinuationSupertype = findBaseContinuationSuperSupertype(continuationType) ?: return
val debugMetadataKtType = debugMetadataKtType()
if (debugMetadataKtType is ClassType) {
val location = createLocation(continuation, debugMetadataKtType)
location?.let {
val spilledVariables = getSpilledVariables(continuation, debugMetadataKtType) ?: emptyList()
consumer.add(DefaultCoroutineStackFrameItem(location, spilledVariables))
}
val completionField = baseContinuationSupertype.fieldByName("completion") ?: return
val completion = continuation.getValue(completionField) as? ObjectReference ?: return
collectFramesRecursively(completion, consumer)
}
}
fun debugMetadataKtType(): ClassType? {
val debugMetadataKtType = context.findClassSafe(DEBUG_METADATA_KT)
if (debugMetadataKtType != null) {
return debugMetadataKtType
} else {
log.warn("Continuation information found but no $DEBUG_METADATA_KT class exists. Please check kotlin-stdlib version.")
}
return null
}
private fun createLocation(continuation: ObjectReference, debugMetadataKtType: ClassType): GeneratedLocation? {
val instance = invokeGetStackTraceElement(continuation, debugMetadataKtType) ?: return null
val className = context.invokeMethodAsString(instance, "getClassName") ?: return null
val methodName = context.invokeMethodAsString(instance, "getMethodName") ?: return null
val lineNumber = context.invokeMethodAsInt(instance, "getLineNumber")?.takeIf {
it >= 0
} ?: return null
val locationClass = context.findClassSafe(className) ?: return null
return GeneratedLocation(context.debugProcess, locationClass, methodName, lineNumber)
}
private fun invokeGetStackTraceElement(continuation: ObjectReference, debugMetadataKtType: ClassType): ObjectReference? {
val stackTraceElement =
context.invokeMethodAsObject(debugMetadataKtType, "getStackTraceElement", continuation) ?: return null
stackTraceElement.referenceType().takeIf { it.name() == StackTraceElement::class.java.name } ?: return null
context.keepReference(stackTraceElement)
return stackTraceElement
}
fun getSpilledVariables(continuation: ObjectReference): List<XNamedValue>? {
debugMetadataKtType()?.let {
return getSpilledVariables(continuation, it)
}
return null
}
fun getSpilledVariables(continuation: ObjectReference, debugMetadataKtType: ClassType): List<XNamedValue>? {
val rawSpilledVariables =
context.invokeMethodAsArray(
debugMetadataKtType,
"getSpilledVariableFieldMapping",
"(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)[Ljava/lang/String;",
continuation
) ?: 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,239 @@
/*
* Copyright 2010-2020 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.coroutine.proxy
import com.intellij.debugger.engine.JavaValue
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.jdi.GeneratedLocation
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.xdebugger.frame.XNamedValue
import com.intellij.xdebugger.frame.XStackFrame
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.coroutine.data.ContinuationValueDescriptorImpl
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineStackFrameItem
import org.jetbrains.kotlin.idea.debugger.coroutine.data.DefaultCoroutineStackFrameItem
import org.jetbrains.kotlin.idea.debugger.coroutine.standaloneCoroutineDebuggerEnabled
import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger
import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext
import org.jetbrains.kotlin.idea.debugger.invokeInManagerThread
data class ContinuationHolder(val continuation: ObjectReference, val context: DefaultExecutionContext) {
val log by logger
fun getAsyncStackTraceIfAny(): CoroutineHolder? {
try {
return collectFrames()
} catch (e: Exception) {
log.error("Error while looking for variables.", e)
}
return null
}
private fun collectFrames(): CoroutineHolder? {
val consumer = mutableListOf<CoroutineStackFrameItem>()
var completion = this
val debugMetadataKtType = debugMetadataKtType() ?: return null
while (completion.isBaseContinuationImpl()) {
val coroutineStackFrame = context.debugProcess.invokeInManagerThread {
createLocation(completion, debugMetadataKtType)
}
if (coroutineStackFrame != null) {
consumer.add(coroutineStackFrame)
}
completion = completion.findCompletion() ?: break
}
if (completion.value().type().isAbstractCoroutine())
return CoroutineHolder.lookup(completion.value(), context, consumer)
else {
log.warn("AbstractCoroutine not found, ${completion.value().type()} is not subtype of AbstractCoroutine as expected.")
return CoroutineHolder.lookup(null, context, consumer)
}
}
private fun createLocation(continuation: ContinuationHolder, debugMetadataKtType: ClassType): DefaultCoroutineStackFrameItem? {
val instance = invokeGetStackTraceElement(continuation, debugMetadataKtType) ?: return null
val className = context.invokeMethodAsString(instance, "getClassName") ?: return null
val methodName = context.invokeMethodAsString(instance, "getMethodName") ?: return null
val lineNumber = context.invokeMethodAsInt(instance, "getLineNumber")?.takeIf {
it >= 0
} ?: return null // skip invokeSuspend:-1
val locationClass = context.findClassSafe(className) ?: return null
val generatedLocation = GeneratedLocation(context.debugProcess, locationClass, methodName, lineNumber)
val spilledVariables = getSpilledVariables(continuation, debugMetadataKtType) ?: emptyList()
return DefaultCoroutineStackFrameItem(generatedLocation, spilledVariables)
}
private fun invokeGetStackTraceElement(continuation: ContinuationHolder, debugMetadataKtType: ClassType): ObjectReference? {
val stackTraceElement =
context.invokeMethodAsObject(debugMetadataKtType, "getStackTraceElement", continuation.value()) ?: return null
stackTraceElement.referenceType().takeIf { it.name() == StackTraceElement::class.java.name } ?: return null
context.keepReference(stackTraceElement)
return stackTraceElement
}
fun getSpilledVariables(): List<XNamedValue>? {
debugMetadataKtType()?.let {
return getSpilledVariables(this, it)
}
return null
}
private fun getSpilledVariables(continuation: ContinuationHolder, debugMetadataKtType: ClassType): List<XNamedValue>? {
val variables: List<JavaValue> = context.debugProcess.invokeInManagerThread {
FieldVariable.extractFromContinuation(context, continuation.value(), debugMetadataKtType).map {
val valueDescriptor = ContinuationValueDescriptorImpl(
context.project,
continuation,
it.fieldName,
it.variableName
)
JavaValue.create(
null,
valueDescriptor,
context.evaluationContext,
context.debugProcess.xdebugProcess!!.nodeManager,
false
)
}
} ?: emptyList()
return variables
}
private fun debugMetadataKtType(): ClassType? {
val debugMetadataKtType = context.findCoroutineMetadataType()
if (debugMetadataKtType == null)
log.warn("Continuation information found but no 'kotlin.coroutines.jvm.internal.DebugMetadataKt' class exists. Please check kotlin-stdlib version.")
return debugMetadataKtType
}
fun referenceType(): ClassType? =
continuation.referenceType() as? ClassType
fun value() =
continuation
fun field(field: Field): Value? =
continuation.getValue(field)
fun findCompletion(): ContinuationHolder? {
val type = continuation.type()
if (type is ClassType && type.isBaseContinuationImpl()) {
val completionField = type.completionField() ?: return null
return ContinuationHolder(continuation.getValue(completionField) as? ObjectReference ?: return null, context)
}
return null
}
fun isBaseContinuationImpl() =
continuation.type().isBaseContinuationImpl()
companion object {
val log by logger
fun lookupForResumeMethodContinuation(
suspendContext: SuspendContextImpl,
frame: StackFrameProxyImpl
): ContinuationHolder? {
if (frame.location().isPreExitFrame()) {
val context = suspendContext.executionContext() ?: return null
var continuation = frame.completionVariableValue() ?: return null
context.keepReference(continuation)
return ContinuationHolder(continuation, context)
} else
return null
}
fun coroutineExitFrame(
frame: StackFrameProxyImpl,
suspendContext: SuspendContextImpl
): XStackFrame? {
return suspendContext.invokeInManagerThread {
if (frame.location().isPreFlight()) {
if(standaloneCoroutineDebuggerEnabled())
log.trace("Entry frame found: ${formatLocation(frame.location())}")
constructPreFlightFrame(frame, suspendContext)
} else
null
}
}
fun constructPreFlightFrame(
invokeSuspendFrame: StackFrameProxyImpl,
suspendContext: SuspendContextImpl
): CoroutinePreflightStackFrame? {
var frames = invokeSuspendFrame.threadProxy().frames()
val indexOfCurrentFrame = frames.indexOf(invokeSuspendFrame)
val indexofgetcoroutineSuspended = findGetCoroutineSuspended(frames)
// @TODO if found - skip this thread stack
if (indexOfCurrentFrame > 0 && frames.size > indexOfCurrentFrame && indexofgetcoroutineSuspended < 0) {
val resumeWithFrame = frames[indexOfCurrentFrame + 1] ?: return null
val ch = lookupForResumeMethodContinuation(suspendContext, resumeWithFrame) ?: return null
val coroutineStackTrace = ch.getAsyncStackTraceIfAny() ?: return null
return CoroutinePreflightStackFrame.preflight(
invokeSuspendFrame,
resumeWithFrame,
coroutineStackTrace.stackFrameItems,
frames.drop(indexOfCurrentFrame)
)
}
return null
}
private fun formatLocation(location: Location): String {
return "${location.method().name()}:${location.lineNumber()}, ${location.method().declaringType()} in ${location.sourceName()}"
}
/**
* 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 lookup(
context: SuspendContextImpl,
initialContinuation: ObjectReference?,
// frame: StackTraceElement,
// threadProxy: ThreadReferenceProxyImpl
): ContinuationHolder? {
var continuation = initialContinuation ?: return null
// val classLine = ClassNameLineNumber(frame.className, frame.lineNumber)
val executionContext = context.executionContext() ?: return null
do {
// val position = getClassAndLineNumber(executionContext, continuation)
// while continuation is BaseContinuationImpl and it's frame equals to the current
continuation = getNextFrame(executionContext, continuation) ?: return null
} while (continuation.type().isBaseContinuationImpl() /* && position != classLine */)
return if (continuation.type().isBaseContinuationImpl())
ContinuationHolder(continuation, executionContext)
else
return null
}
data class ClassNameLineNumber(val className: String?, val lineNumber: Int?)
//
// private fun getClassAndLineNumber(context: ExecutionContext, continuation: ObjectReference): ClassNameLineNumber {
// val objectReference = findStackTraceElement(context, continuation) ?: return ClassNameLineNumber(null, null)
// val classStackTraceElement = context.findClass("java.lang.StackTraceElement") as ClassType
// val getClassName = classStackTraceElement.concreteMethodByName("getClassName", "()Ljava/lang/String;")
// val getLineNumber = classStackTraceElement.concreteMethodByName("getLineNumber", "()I")
// val className = (context.invokeMethod(objectReference, getClassName, emptyList()) as StringReference).value()
// val lineNumber = (context.invokeMethod(objectReference, getLineNumber, emptyList()) as IntegerValue).value()
// return ClassNameLineNumber(className, lineNumber)
// }
// private fun findStackTraceElement(context: ExecutionContext, continuation: ObjectReference): ObjectReference? {
// val classType = continuation.type() as ClassType
// val methodGetStackTraceElement = classType.concreteMethodByName("getStackTraceElement", "()Ljava/lang/StackTraceElement;")
// return context.invokeMethod(continuation, methodGetStackTraceElement, emptyList()) as? ObjectReference
// }
}
}
@@ -4,48 +4,29 @@
*/
package org.jetbrains.kotlin.idea.debugger.coroutine.proxy
import com.intellij.debugger.engine.DebuggerManagerThreadImpl
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.xdebugger.frame.XSuspendContext
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.coroutine.command.CoroutineBuilder
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoCache
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData
import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger
import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
class CoroutineDebugProbesProxy(val suspendContext: XSuspendContext) {
class CoroutineDebugProbesProxy(val suspendContext: SuspendContextImpl) {
private val log by logger
private var executionContext: DefaultExecutionContext = executionContext()
// 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 {
fun dumpCoroutines(): CoroutineInfoCache {
DebuggerManagerThreadImpl.assertIsManagerThread()
val coroutineInfoCache = CoroutineInfoCache()
try {
val infoList = dump()
val executionContext = suspendContext.executionContext() ?: return coroutineInfoCache.fail()
val libraryAgentProxy = findProvider(executionContext)
val infoList = libraryAgentProxy.dumpCoroutinesInfo()
coroutineInfoCache.ok(infoList)
} catch (e: Throwable) {
log.error("Exception is thrown by calling dumpCoroutines.", e)
@@ -54,189 +35,8 @@ class CoroutineDebugProbesProxy(val suspendContext: XSuspendContext) {
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)
private fun findProvider(executionContext: DefaultExecutionContext) =
CoroutineLibraryAgentProxy.instance(executionContext) ?: CoroutineNoLibraryProxy(executionContext)
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
val stackTrace = getStackTrace(instance)
return CoroutineInfoData(
name,
CoroutineInfoData.State.valueOf(state),
stackTrace,
thread,
lastObservedFrameFieldRef
)
}
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()
private fun executionContext() : DefaultExecutionContext {
return DefaultExecutionContext(suspendContext as SuspendContextImpl, suspendContext.frameProxy)
}
/**
* @TODO refactor later
* Holds ClassTypes, Methods, ObjectReferences and Fields for a particular jvm
*/
class ProcessReferences(executionContext: DefaultExecutionContext) {
// 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
}
}
@@ -0,0 +1,64 @@
/*
* Copyright 2010-2020 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.coroutine.proxy
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineNameIdState
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineStackFrameItem
import org.jetbrains.kotlin.idea.debugger.coroutine.data.State
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.mirror.JavaLangMirror
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.mirror.MirrorOfCoroutineContext
import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext
data class CoroutineHolder(
val value: ObjectReference?,
val info: CoroutineNameIdState,
val stackFrameItems: List<CoroutineStackFrameItem>
) {
companion object {
fun lookup(
value: ObjectReference?,
context: DefaultExecutionContext,
stackFrameItems: List<CoroutineStackFrameItem>
): CoroutineHolder? {
val state = state(value, context) ?: return null
val realState =
if (stackFrameItems.isEmpty()) state.copy(state = State.CREATED) else state.copy(state = State.SUSPENDED)
return CoroutineHolder(value, realState, stackFrameItems)
}
fun state(value: ObjectReference?, context: DefaultExecutionContext): CoroutineNameIdState? {
value ?: return null
val reference = JavaLangMirror(context)
val standAloneCoroutineMirror = reference.standaloneCoroutine.mirror(value, context)
if (standAloneCoroutineMirror?.context is MirrorOfCoroutineContext) {
val id = standAloneCoroutineMirror.context.id
val toString = reference.string(value, context)
val r = """\w+\{(\w+)\}\@([\w\d]+)""".toRegex()
val matcher = r.toPattern().matcher(toString)
if (matcher.matches()) {
val state = stateOf(matcher.group(1))
val hexAddress = matcher.group(2)
return CoroutineNameIdState(standAloneCoroutineMirror.context.name, id?.toString() ?: hexAddress, state)
}
}
return null
}
private fun stateOf(state: String?): State =
when (state) {
"Active" -> State.RUNNING
"Cancelling" -> State.SUSPENDED_CANCELLING
"Completing" -> State.SUSPENDED_COMPLETING
"Cancelled" -> State.CANCELLED
"Completed" -> State.COMPLETED
"New" -> State.NEW
else -> State.UNKNOWN
}
}
}
@@ -0,0 +1,12 @@
/*
* Copyright 2010-2020 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.coroutine.proxy
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData
interface CoroutineInfoProvider {
fun dumpCoroutinesInfo(): List<CoroutineInfoData>
}
@@ -0,0 +1,220 @@
/*
* Copyright 2010-2020 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.coroutine.proxy
import com.intellij.debugger.engine.DebugProcess
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.jdi.ClassesByNameProvider
import com.intellij.debugger.jdi.GeneratedLocation
import com.intellij.util.containers.ContainerUtil
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.coroutine.data.*
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.mirror.CoroutineContext
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.mirror.JavaLangMirror
import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext
class CoroutineLibraryAgentProxy(private val debugProbesClsRef: ClassType, private val executionContext: DefaultExecutionContext) :
CoroutineInfoProvider {
private val coroutineContextReference: JavaLangMirror = JavaLangMirror(executionContext)
private val debugProbesImplClsRef = executionContext.findClass("$DEBUG_PACKAGE.internal.DebugProbesImpl") as ClassType
private val debugProbesImplInstance = with(debugProbesImplClsRef) { getValue(fieldByName("INSTANCE")) as ObjectReference }
private val enhanceStackTraceWithThreadDumpRef: Method = debugProbesImplClsRef
.methodsByName("enhanceStackTraceWithThreadDump").single()
private val dumpMethod: Method = debugProbesClsRef.concreteMethodByName("dumpCoroutinesInfo", "()Ljava/util/List;")
val instance = with(debugProbesClsRef) { getValue(fieldByName("INSTANCE")) as ObjectReference }
// CoroutineInfo
private val coroutineInfoClsRef = executionContext.findClass("$DEBUG_PACKAGE.CoroutineInfo") as ClassType
private val getStateRef: Method = coroutineInfoClsRef.concreteMethodByName("getState", "()Lkotlinx/coroutines/debug/State;")
private val getContextRef: Method = coroutineInfoClsRef.concreteMethodByName("getContext", "()Lkotlin/coroutines/CoroutineContext;")
private val lastObservedStackTraceRef: Method = coroutineInfoClsRef.methodsByName("lastObservedStackTrace").single()
private val sequenceNumberFieldRef: Field = coroutineInfoClsRef.fieldByName("sequenceNumber")
private val lastObservedThreadFieldRef: Field = coroutineInfoClsRef.fieldByName("lastObservedThread")
private val lastObservedFrameFieldRef: Field = coroutineInfoClsRef.fieldByName("lastObservedFrame") // continuation
// value
private val vm = executionContext.vm
private val classesByName = ClassesByNameProvider.createCache(vm.allClasses())
private val coroutineContext: CoroutineContext = CoroutineContext(executionContext)
@Synchronized
@Suppress("unused")
fun install() =
executionContext.invokeMethodAsVoid(instance, "install")
@Synchronized
@Suppress("unused")
fun uninstall() =
executionContext.invokeMethodAsVoid(instance, "uninstall")
override fun dumpCoroutinesInfo(): List<CoroutineInfoData> {
val coroutinesInfo = executionContext.invokeMethodAsObject(instance, dumpMethod) ?: return emptyList()
executionContext.keepReference(coroutinesInfo)
val size = coroutineContextReference.sizeOf(coroutinesInfo, executionContext)
return MutableList(size) {
val elem = coroutineContextReference.elementFromList(coroutinesInfo, it, executionContext)
fetchCoroutineState(elem)
}
}
private fun fetchCoroutineState(instance: ObjectReference): CoroutineInfoData {
val name = getName(instance)
val state = getState(instance)
val thread = getLastObservedThread(instance, lastObservedThreadFieldRef)
val lastObservedFrameFieldRef = instance.getValue(lastObservedFrameFieldRef) as? ObjectReference
val stackTrace = getStackTrace(instance)
val creationFrameSeparatorIndex = findCreationFrameIndex(stackTrace)
val coroutineStackTrace = stackTrace.take(creationFrameSeparatorIndex)
val coroutineStackTraceFrameItems = coroutineStackTrace.map {
SuspendCoroutineStackFrameItem(it, createLocation(it))
}
val creationStackTrace = stackTrace.subList(creationFrameSeparatorIndex + 1, stackTrace.size)
val creationStackTraceFrameItems = creationStackTrace.map {
CreationCoroutineStackFrameItem(it, createLocation(it))
}
val key = CoroutineNameIdState(name,"", State.valueOf(state))
return CoroutineInfoData(
key,
coroutineStackTraceFrameItems,
creationStackTraceFrameItems,
thread,
lastObservedFrameFieldRef
)
}
private fun createLocation(stackTraceElement: StackTraceElement): Location = 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(executionContext.debugProcess, type, methodName, line)
}
/**
* Tries to find creation frame separator if any, returns last index if none found
*/
private fun findCreationFrameIndex(frames: List<StackTraceElement>): Int {
val index = frames.indexOfFirst { it.isCreationSeparatorFrame() }
return if (index < 0)
frames.lastIndex
else
index
}
private fun getName(
info: ObjectReference // CoroutineInfo instance
): String {
// equals to `coroutineInfo.context.get(CoroutineName).name`
val coroutineContextInst = executionContext.invokeMethod(
info,
getContextRef,
emptyList()
) as? ObjectReference ?: throw IllegalArgumentException("Coroutine context must not be null")
val context = coroutineContext.mirror(coroutineContextInst, executionContext)
val name = context?.name ?: "coroutine"
val id = (info.getValue(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, getStateRef, emptyList()) as ObjectReference
return coroutineContextReference.string(state, executionContext)
}
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>()
// val sizeOfFrameList = coroutineContextReference.sizeOf(frameList, executionContext)
// for (it in 0 until sizeOfFrameList) {
// val frame = coroutineContextReference.elementFromList(frameList, it, executionContext)
// val ste = coroutineContextReference.stackTraceElement(frame)
// tmpList.add(ste)
// }
val mergedFrameList = enhanceStackTraceWithThreadDump(listOf(info, frameList))
val sizeOfMergedFrameList = coroutineContextReference.sizeOf(mergedFrameList, executionContext)
val list = mutableListOf<StackTraceElement>()
for (it in 0 until sizeOfMergedFrameList) {
val frame = coroutineContextReference.elementFromList(mergedFrameList, it, executionContext)
val ste = coroutineContextReference.stackTraceElement(frame)
list.add(// 0, // add in the beginning // @TODO what's the point?
ste
)
}
return list
}
private fun lastObservedStackTrace(instance: ObjectReference) =
executionContext.invokeMethod(instance, lastObservedStackTraceRef, emptyList()) as ObjectReference
private fun enhanceStackTraceWithThreadDump(args: List<ObjectReference>) =
executionContext.invokeMethod(
debugProbesImplInstance,
enhanceStackTraceWithThreadDumpRef, args
) as ObjectReference
companion object {
private const val DEBUG_PACKAGE = "kotlinx.coroutines.debug"
fun instance(executionContext: DefaultExecutionContext): CoroutineLibraryAgentProxy? {
try {
val debugProbesClsRef = executionContext.findClass("$DEBUG_PACKAGE.DebugProbes") ?: return null
if (debugProbesClsRef is ClassType) {
val instanceField = debugProbesClsRef.fieldByName("INSTANCE")
val debugProbes = debugProbesClsRef.getValue(instanceField) as ObjectReference
val f = debugProbes.referenceType().fieldByName("isInstalled")
val debugProbesActivated = if (f != null) (debugProbes.getValue(f) as BooleanValue).value() else true
if (debugProbesActivated)
return CoroutineLibraryAgentProxy(debugProbesClsRef, executionContext)
}
} catch (e: EvaluateException) {
}
return null
}
}
}
@@ -0,0 +1,86 @@
/*
* Copyright 2010-2020 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.coroutine.proxy
import com.intellij.openapi.util.registry.Registry
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.mirror.CancellableContinuationImpl
import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger
import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext
class CoroutineNoLibraryProxy(val executionContext: DefaultExecutionContext) : CoroutineInfoProvider {
val log by logger
val debugMetadataKtType = executionContext.findCoroutineMetadataType()
override fun dumpCoroutinesInfo(): List<CoroutineInfoData> {
val vm = executionContext.vm
val resultList = mutableListOf<CoroutineInfoData>()
if (vm.virtualMachine.canGetInstanceInfo()) {
when (coroutineSwitch()) {
"DISPATCHED_CONTINUATION" -> dispatchedContinuation(resultList)
"CANCELLABLE_CONTINUATION" -> cancellableContinuation(resultList)
else -> dispatchedContinuation(resultList)
}
} else
log.warn("Remote JVM doesn't support canGetInstanceInfo capability (perhaps JDK-8197943).")
return resultList
}
private fun cancellableContinuation(resultList: MutableList<CoroutineInfoData>): Boolean {
val dcClassTypeList = executionContext.findCancellableContinuationImplReferenceType()
if (dcClassTypeList?.size == 1) {
val dcClassType = dcClassTypeList.first()
val cci = CancellableContinuationImpl(executionContext)
val continuationList = dcClassType.instances(maxCoroutines())
for (cancellableContinuation in continuationList) {
val coroutineInfo = extractCancellableContinuation(cancellableContinuation, cci) ?: continue
resultList.add(coroutineInfo)
}
}
return false
}
fun extractCancellableContinuation(
dispatchedContinuation: ObjectReference,
ccMirrorProvider: CancellableContinuationImpl
): CoroutineInfoData? {
val mirror = ccMirrorProvider.mirror(dispatchedContinuation, executionContext) ?: return null
val continuation = mirror.delegate?.continuation ?: return null
val ch = ContinuationHolder(continuation, executionContext)
val coroutineWithRestoredStack = ch.getAsyncStackTraceIfAny() ?: return null
return CoroutineInfoData.suspendedCoroutineInfoData(coroutineWithRestoredStack, continuation)
}
private fun dispatchedContinuation(resultList: MutableList<CoroutineInfoData>): Boolean {
val dcClassTypeList = executionContext.findDispatchedContinuationReferenceType()
if (dcClassTypeList?.size == 1) {
val dcClassType = dcClassTypeList.first()
val continuationField = dcClassType.fieldByName("continuation") ?: return true
val continuationList = dcClassType.instances(maxCoroutines())
for (dispatchedContinuation in continuationList) {
val coroutineInfo = extractDispatchedContinuation(dispatchedContinuation, continuationField) ?: continue
resultList.add(coroutineInfo)
}
}
return false
}
fun extractDispatchedContinuation(dispatchedContinuation: ObjectReference, continuation: Field): CoroutineInfoData? {
debugMetadataKtType ?: return null
val initialContinuation = dispatchedContinuation.getValue(continuation) as ObjectReference
val ch = ContinuationHolder(initialContinuation, executionContext)
val coroutineWithRestoredStack = ch.getAsyncStackTraceIfAny() ?: return null
return CoroutineInfoData.suspendedCoroutineInfoData(coroutineWithRestoredStack, initialContinuation)
}
}
fun maxCoroutines() = Registry.intValue("kotlin.debugger.coroutines.max", 1000).toLong()
fun coroutineSwitch() = Registry.stringValue("kotlin.debugger.coroutines.switch")
@@ -0,0 +1,65 @@
/*
* Copyright 2010-2020 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.coroutine.proxy
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.DebuggerManagerThreadImpl
import com.intellij.debugger.engine.JVMStackFrameInfoProvider
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.ui.impl.watch.MethodsTracker
import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl
import com.intellij.xdebugger.frame.XCompositeNode
import com.intellij.xdebugger.frame.XValueChildrenList
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineStackFrameItem
import org.jetbrains.kotlin.idea.debugger.invokeInManagerThread
import org.jetbrains.kotlin.idea.debugger.stackFrame.KotlinStackFrame
/**
* Coroutine exit frame represented by a stack frames
* invokeSuspend():-1
* resumeWith()
*
*/
class CoroutinePreflightStackFrame(
val invokeSuspendFrame: StackFrameProxyImpl,
val resumeWithFrame: StackFrameProxyImpl,
val restoredStackFrame: List<CoroutineStackFrameItem>,
val stackFrameDescriptorImpl: StackFrameDescriptorImpl,
val threadPreCoroutineFrames: List<StackFrameProxyImpl>
) : KotlinStackFrame(stackFrameDescriptorImpl), JVMStackFrameInfoProvider {
override fun computeChildren(node: XCompositeNode) {
val childrenList = XValueChildrenList()
val firstRestoredCoroutineStackFrameItem = restoredStackFrame.firstOrNull() ?: return
firstRestoredCoroutineStackFrameItem.spilledVariables.forEach {
childrenList.add(it)
}
node.addChildren(childrenList, false)
super.computeChildren(node)
}
override fun isInLibraryContent() =
false
override fun isSynthetic() =
false
companion object {
fun preflight(
invokeSuspendFrame: StackFrameProxyImpl,
resumeWithFrame: StackFrameProxyImpl,
restoredStackFrame: List<CoroutineStackFrameItem>,
originalFrames: List<StackFrameProxyImpl>
): CoroutinePreflightStackFrame? {
val topRestoredFrame = restoredStackFrame.firstOrNull() ?: return null
val descriptor = StackFrameDescriptorImpl(
LocationStackFrameProxyImpl(topRestoredFrame.location, invokeSuspendFrame), MethodsTracker()
)
return CoroutinePreflightStackFrame(invokeSuspendFrame, resumeWithFrame, restoredStackFrame, descriptor, originalFrames)
}
}
}
@@ -0,0 +1,133 @@
/*
* Copyright 2010-2020 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.coroutine.proxy
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl
import com.intellij.openapi.project.Project
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.xdebugger.XDebugSession
import com.intellij.xdebugger.XDebuggerUtil
import com.intellij.xdebugger.XSourcePosition
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.*
import org.jetbrains.kotlin.idea.debugger.coroutine.command.CoroutineBuilder
import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext
import org.jetbrains.kotlin.idea.util.application.isUnitTestMode
fun Method.isInvokeSuspend(): Boolean =
name() == "invokeSuspend" && signature() == "(Ljava/lang/Object;)Ljava/lang/Object;"
fun Method.isContinuation() =
isInvokeSuspend() && declaringType().isContinuation() /* Perhaps need to check for "Lkotlin/coroutines/Continuation;)" in signature() ? */
fun Method.isSuspendLambda() =
isInvokeSuspend() && declaringType().isSuspendLambda()
fun Method.isResumeWith() =
name() == "resumeWith" && signature() == "(Ljava/lang/Object;)V" && (declaringType().isSuspendLambda() || declaringType().isContinuation())
fun Location.isPreFlight(): Boolean {
val method = safeMethod() ?: return false
return method.isSuspendLambda() || method.isContinuation()
}
fun ReferenceType.isContinuation() =
isBaseContinuationImpl() || isSubtype("kotlin.coroutines.Continuation")
fun Type.isBaseContinuationImpl() =
isSubtype("kotlin.coroutines.jvm.internal.BaseContinuationImpl")
fun Type.isAbstractCoroutine() =
isSubtype("kotlinx.coroutines.AbstractCoroutine")
fun Type.isSubTypeOrSame(className: String) =
name() == className || isSubtype(className)
fun ReferenceType.isSuspendLambda() =
SUSPEND_LAMBDA_CLASSES.any { isSubtype(it) }
fun Location.isPreExitFrame() =
safeMethod()?.isResumeWith() ?: false
fun StackFrameProxyImpl.variableValue(variableName: String): ObjectReference? {
val continuationVariable = safeVisibleVariableByName(variableName) ?: return null
return getValue(continuationVariable) as? ObjectReference ?: return null
}
fun StackFrameProxyImpl.completionVariableValue(): ObjectReference? =
variableValue("completion")
private fun Method.isGetCOROUTINE_SUSPENDED() =
signature() == "()Ljava/lang/Object;" && name() == "getCOROUTINE_SUSPENDED" && declaringType().name() == "kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsKt"
fun DefaultExecutionContext.findCoroutineMetadataType() =
debugProcess.invokeInManagerThread { findClassSafe("kotlin.coroutines.jvm.internal.DebugMetadataKt") }
fun DefaultExecutionContext.findCoroutineAnnotationMetadataType() =
debugProcess.invokeInManagerThread { findClassSafe("kotlin.coroutines.jvm.internal.DebugMetadata") as InterfaceType? }
fun DefaultExecutionContext.findDispatchedContinuationReferenceType(): List<ReferenceType>? =
vm.classesByName("kotlinx.coroutines.DispatchedContinuation")
fun DefaultExecutionContext.findCancellableContinuationImplReferenceType(): List<ReferenceType>? =
vm.classesByName("kotlinx.coroutines.CancellableContinuationImpl")
fun findGetCoroutineSuspended(frames: List<StackFrameProxyImpl>) =
frames.indexOfFirst { it.safeLocation()?.safeMethod()?.isGetCOROUTINE_SUSPENDED() == true }
fun StackTraceElement.isCreationSeparatorFrame() =
className.startsWith(CoroutineBuilder.CREATION_STACK_TRACE_SEPARATOR)
fun StackTraceElement.findPosition(project: Project): XSourcePosition? =
getPosition(project, className, lineNumber)
fun Location.findPosition(project: Project) =
getPosition(project, declaringType().name(), lineNumber())
fun ClassType.completionField() =
fieldByName("completion") as Field?
private fun getPosition(project: Project, className: String, lineNumber: Int): XSourcePosition? {
val psiFacade = JavaPsiFacade.getInstance(project)
val psiClass = psiFacade.findClass(
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 localLineNumber = if (lineNumber > 0) lineNumber - 1 else return null
return XDebuggerUtil.getInstance().createPosition(classFile, localLineNumber)
}
/**
* Finds previous Continuation for this Continuation (completion field in BaseContinuationImpl)
* @return null if given ObjectReference is not a BaseContinuationImpl instance or completion is null
*/
fun getNextFrame(context: DefaultExecutionContext, continuation: ObjectReference): ObjectReference? {
if (!continuation.type().isBaseContinuationImpl())
return null
val type = continuation.type() as ClassType
val next = type.concreteMethodByName("getCompletion", "()Lkotlin/coroutines/Continuation;")
return context.invokeMethod(continuation, next, emptyList()) as? ObjectReference
}
fun SuspendContextImpl.executionContext() =
invokeInManagerThread { DefaultExecutionContext(EvaluationContextImpl(this, this.frameProxy)) }
fun <T : Any> SuspendContextImpl.invokeInManagerThread(f: () -> T?) : T? =
debugProcess.invokeInManagerThread { f() }
fun ThreadReferenceProxyImpl.supportsEvaluation(): Boolean =
threadReference?.isSuspended ?: false
fun SuspendContextImpl.supportsEvaluation() =
this.debugProcess.canRunEvaluation || isUnitTestMode()
fun XDebugSession.suspendContextImpl() =
suspendContext as SuspendContextImpl
@@ -0,0 +1,44 @@
/*
* Copyright 2010-2020 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.coroutine.proxy
import com.sun.jdi.ArrayReference
import com.sun.jdi.ClassType
import com.sun.jdi.ObjectReference
import com.sun.jdi.StringReference
import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext
data class FieldVariable(val fieldName: String, val variableName: String) {
companion object {
fun extractFromContinuation(context: DefaultExecutionContext, continuation: ObjectReference, debugMetadataKtType: ClassType?) : List<FieldVariable> {
val metadataType = debugMetadataKtType ?: context.findCoroutineMetadataType() ?: return emptyList()
val rawSpilledVariables =
context.invokeMethodAsArray(
metadataType,
"getSpilledVariableFieldMapping",
"(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)[Ljava/lang/String;",
continuation
) ?: return emptyList()
context.keepReference(rawSpilledVariables)
val length = rawSpilledVariables.length() / 2
val fieldVariables = ArrayList<FieldVariable>()
for (index in 0 until length) {
fieldVariables.add(getFieldVariableName(rawSpilledVariables, index) ?: continue)
}
return fieldVariables
}
private fun getFieldVariableName(rawSpilledVariables: ArrayReference, index: Int): FieldVariable? {
val fieldName = (rawSpilledVariables.getValue(2 * index) as? StringReference)?.value() ?: return null
val variableName = (rawSpilledVariables.getValue(2 * index + 1) as? StringReference)?.value() ?: return null
return FieldVariable(fieldName, variableName)
}
}
}
@@ -0,0 +1,16 @@
/*
* Copyright 2010-2020 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.coroutine.proxy
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.sun.jdi.Location
class LocationStackFrameProxyImpl(val location: Location, frame: StackFrameProxyImpl) :
StackFrameProxyImpl(frame.threadProxy(), frame.stackFrame, frame.indexFromBottom) {
override fun location(): Location {
return location
}
}
@@ -1,94 +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.coroutine.proxy
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext
import org.jetbrains.kotlin.idea.debugger.isSubtype
class LookupContinuation(val context: DefaultExecutionContext, 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)
}
/**
* 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 findContinuation(initialContinuation: ObjectReference?): ObjectReference? {
if (!isApplicable())
return null
var continuation = initialContinuation ?: return null
val baseType = "kotlin.coroutines.jvm.internal.BaseContinuationImpl"
val getTrace = (continuation.type() as ClassType).concreteMethodByName(
"getStackTraceElement",
"()Ljava/lang/StackTraceElement;"
)
val stackTraceType = context.findClass("java.lang.StackTraceElement") as ClassType
val getClassName = stackTraceType.concreteMethodByName("getClassName", "()Ljava/lang/String;")
val getLineNumber = stackTraceType.concreteMethodByName("getLineNumber", "()I")
val className = {
val trace = context.invokeMethod(continuation, getTrace, emptyList()) as? ObjectReference
if (trace != null)
(context.invokeMethod(trace, getClassName, emptyList()) as StringReference).value()
else null
}
val lineNumber = {
val trace = context.invokeMethod(continuation, getTrace, emptyList()) as? ObjectReference
if (trace != null)
(context.invokeMethod(trace, getLineNumber, emptyList()) as IntegerValue).value()
else null
}
while (continuation.type().isSubtype(baseType)
&& (frame.className != className() || frame.lineNumber != lineNumber())
) {
// while continuation is BaseContinuationImpl and it's frame equals to the current
continuation = getNextFrame(continuation, context) ?: return null
}
return if (continuation.type().isSubtype(baseType)) continuation else null
}
/**
* 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: DefaultExecutionContext): 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
}
fun findGetStackTraceElementMethodRef(continuation: ObjectReference): Method =
(continuation.type() as ClassType).concreteMethodByName("getStackTraceElement", "()Ljava/lang/StackTraceElement;")
fun createAsyncStackTraceContext(stackTraceElementMethodRef: Method) =
AsyncStackTraceContext(context, stackTraceElementMethodRef)
fun createAsyncStackTraceContext(continuation: ObjectReference): AsyncStackTraceContext? {
val getStackTraceElementMethodRef = findGetStackTraceElementMethodRef(continuation)
return createAsyncStackTraceContext(getStackTraceElementMethodRef)
}
}
@@ -6,28 +6,34 @@
package org.jetbrains.kotlin.idea.debugger.coroutine.proxy
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.JavaDebugProcess
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.engine.events.DebuggerCommandImpl
import com.intellij.debugger.engine.events.SuspendContextCommandImpl
import com.intellij.debugger.impl.PrioritizedTask
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.util.Computable
import com.intellij.xdebugger.XDebugProcess
import com.intellij.xdebugger.XDebugSession
import com.intellij.xdebugger.frame.XSuspendContext
import java.awt.Component
class ManagerThreadExecutor(val debugProcess: DebugProcessImpl) {
fun on(suspendContext: SuspendContextImpl, priority: PrioritizedTask.Priority = PrioritizedTask.Priority.NORMAL) =
ManagerThreadExecutorInstance(suspendContext, priority)
constructor(session: XDebugSession) : this(session.debugProcess)
constructor(debugProcess: XDebugProcess) : this((debugProcess as JavaDebugProcess).debuggerSession.process)
fun on(suspendContext: XSuspendContext, priority: PrioritizedTask.Priority = PrioritizedTask.Priority.NORMAL) =
ManagerThreadExecutorInstance(suspendContext as SuspendContextImpl, priority)
ManagerThreadExecutorInstance(suspendContext, priority)
inner class ManagerThreadExecutorInstance(
val suspendContext: SuspendContextImpl,
val priority: PrioritizedTask.Priority = PrioritizedTask.Priority.NORMAL
) {
constructor(sc: XSuspendContext, priority: PrioritizedTask.Priority) : this(sc as SuspendContextImpl, priority)
fun schedule(f: (SuspendContextImpl) -> Unit) {
val suspendContextCommand = object : SuspendContextCommandImpl(suspendContext) {
override fun getPriority() = this@ManagerThreadExecutorInstance.priority
@@ -0,0 +1,73 @@
/*
* Copyright 2010-2020 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.coroutine.proxy.mirror
import com.sun.jdi.Method
import com.sun.jdi.ObjectReference
import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext
class CoroutineContext(context: DefaultExecutionContext) :
BaseMirror<MirrorOfCoroutineContext>("kotlin.coroutines.CoroutineContext", context) {
val coroutineNameRef = CoroutineName(context)
val coroutineIdRef = CoroutineId(context)
val jobRef = Job(context)
val getContextElement: Method = makeMethod("get")
override fun fetchMirror(value: ObjectReference, context: DefaultExecutionContext): MirrorOfCoroutineContext? {
val coroutineName = getElementValue(value, context, coroutineNameRef) ?: "coroutine"
val coroutineId = getElementValue(value, context, coroutineIdRef)
val job = getElementValue(value, context, jobRef)
return MirrorOfCoroutineContext(value, coroutineName, coroutineId, job)
}
fun <T> getElementValue(value: ObjectReference, context: DefaultExecutionContext, keyProvider: ContextKey<T>): T? {
val elementValue = objectValue(value, getContextElement, context, keyProvider.key()) ?: return null
return keyProvider.mirror(elementValue, context)
}
}
data class MirrorOfCoroutineContext(
val that: ObjectReference,
val name: String,
val id: Long?,
val job: ObjectReference?
)
abstract class ContextKey<T>(name: String, context: DefaultExecutionContext) : BaseMirror<T>(name, context) {
abstract fun key() : ObjectReference
}
class CoroutineName(context: DefaultExecutionContext) : ContextKey<String>("kotlinx.coroutines.CoroutineName", context) {
val key = staticObjectValue("Key")
val getNameRef: Method = makeMethod("getName")
override fun fetchMirror(value: ObjectReference, context: DefaultExecutionContext): String? {
return stringValue(value, getNameRef, context)
}
override fun key() = key
}
class CoroutineId(context: DefaultExecutionContext) : ContextKey<Long>("kotlinx.coroutines.CoroutineId", context) {
val key = staticObjectValue("Key")
val getIdRef: Method = makeMethod("getId")
override fun fetchMirror(value: ObjectReference, context: DefaultExecutionContext): Long? {
return longValue(value, getIdRef, context)
}
override fun key() = key
}
class Job(context: DefaultExecutionContext) : ContextKey<ObjectReference>("kotlinx.coroutines.Job", context) {
val key = staticObjectValue("Key")
override fun fetchMirror(value: ObjectReference, context: DefaultExecutionContext): ObjectReference? {
return value
}
override fun key() = key
}
@@ -0,0 +1,146 @@
/*
* Copyright 2010-2020 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.coroutine.proxy.mirror
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.isSubTypeOrSame
import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger
import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext
abstract class BaseMirror<T>(val name: String, context: DefaultExecutionContext) {
val log by logger
protected val cls = context.findClass(name) ?: throw IllegalStateException("Can't find class ${name} in remote jvm.")
fun makeField(fieldName: String): Field =
cls.fieldByName(fieldName) // childContinuation
fun makeMethod(methodName: String): Method =
cls.methodsByName(methodName).single()
fun isCompatible(value: ObjectReference) =
value.referenceType().isSubTypeOrSame(name)
fun mirror(value: ObjectReference, context: DefaultExecutionContext): T? {
if (!isCompatible(value)) {
log.warn("Value ${value.referenceType()} is not compatible with $name.")
return null
} else
return fetchMirror(value, context)
}
fun staticObjectValue(fieldName: String): ObjectReference {
val keyFieldRef = makeField(fieldName)
return cls.getValue(keyFieldRef) as ObjectReference
}
fun stringValue(value: ObjectReference, field: Field) =
(value.getValue(field) as StringReference).value()
fun stringValue(value: ObjectReference, method: Method, context: DefaultExecutionContext) =
(context.invokeMethod(value, method, emptyList()) as StringReference).value()
fun objectValue(value: ObjectReference, method: Method, context: DefaultExecutionContext, vararg values: Value) =
context.invokeMethodAsObject(value, method, *values)
fun longValue(value: ObjectReference, method: Method, context: DefaultExecutionContext, vararg values: Value) =
(context.invokeMethodAsObject(value, method, *values) as LongValue).longValue()
fun objectValue(value: ObjectReference, field: Field) =
value.getValue(field) as ObjectReference
fun intValue(value: ObjectReference, field: Field) =
(value.getValue(field) as IntegerValue).intValue()
fun longValue(value: ObjectReference, field: Field) =
(value.getValue(field) as LongValue).longValue()
protected abstract fun fetchMirror(value: ObjectReference, context: DefaultExecutionContext): T?
}
class StandaloneCoroutine(context: DefaultExecutionContext) :
BaseMirror<MirrorOfStandaloneCoroutine>("kotlinx.coroutines.StandaloneCoroutine", context) {
private val coroutineContextMirror = CoroutineContext(context)
private val childContinuationMirror = ChildContinuation(context)
private val stateFieldRef: Field = makeField("_state") // childContinuation
private val contextFieldRef: Field = makeField("context")
override fun fetchMirror(value: ObjectReference, context: DefaultExecutionContext): MirrorOfStandaloneCoroutine {
val state = objectValue(value, stateFieldRef)
val childcontinuation = childContinuationMirror.mirror(state, context)
val cc = objectValue(value, contextFieldRef)
val coroutineContext = coroutineContextMirror.mirror(cc, context)
return MirrorOfStandaloneCoroutine(value, childcontinuation, coroutineContext)
}
}
data class MirrorOfStandaloneCoroutine(
val that: ObjectReference,
val state: MirrorOfChildContinuation?,
val context: MirrorOfCoroutineContext?
)
class ChildContinuation(context: DefaultExecutionContext) :
BaseMirror<MirrorOfChildContinuation>("kotlinx.coroutines.ChildContinuation", context) {
private val childContinuationMirror = CancellableContinuationImpl(context)
private val childFieldRef: Field = makeField("child") // cancellableContinuationImpl
override fun fetchMirror(value: ObjectReference, context: DefaultExecutionContext): MirrorOfChildContinuation? {
val child = objectValue(value, childFieldRef)
return MirrorOfChildContinuation(value, childContinuationMirror.mirror(child, context))
}
}
data class MirrorOfChildContinuation(
val that: ObjectReference,
val child: MirrorOfCancellableContinuationImpl?
)
class CancellableContinuationImpl(context: DefaultExecutionContext) :
BaseMirror<MirrorOfCancellableContinuationImpl>("kotlinx.coroutines.CancellableContinuationImpl", context) {
private val coroutineContextMirror = CoroutineContext(context)
private val dispatchedContinuationtMirror = DispatchedContinuation(context)
private val decisionFieldRef: Field = makeField("_decision")
private val delegateFieldRef: Field = makeField("delegate") // DispatchedContinuation
private val resumeModeFieldRef: Field = makeField("resumeMode")
private val submissionTimeFieldRef: Field = makeField("submissionTime")
private val contextFieldRef: Field = makeField("context")
override fun fetchMirror(value: ObjectReference, context: DefaultExecutionContext): MirrorOfCancellableContinuationImpl? {
val decision = intValue(value, decisionFieldRef)
val dispatchedContinuation = dispatchedContinuationtMirror.mirror(objectValue(value, delegateFieldRef), context)
val submissionTime = longValue(value, submissionTimeFieldRef)
val resumeMode = intValue(value, resumeModeFieldRef)
val coroutineContext = objectValue(value, contextFieldRef)
val contextMirror = coroutineContextMirror.mirror(coroutineContext, context)
return MirrorOfCancellableContinuationImpl(value, decision, dispatchedContinuation, resumeMode, submissionTime, contextMirror)
}
}
data class MirrorOfCancellableContinuationImpl(
val that: ObjectReference,
val decision: Int,
val delegate: MirrorOfDispatchedContinuation?,
val resumeMode: Int,
val submissionTyme: Long,
val jobContext: MirrorOfCoroutineContext?
)
class DispatchedContinuation(context: DefaultExecutionContext) :
BaseMirror<MirrorOfDispatchedContinuation>("kotlinx.coroutines.DispatchedContinuation", context) {
private val decisionFieldRef: Field = makeField("continuation")
override fun fetchMirror(value: ObjectReference, context: DefaultExecutionContext): MirrorOfDispatchedContinuation? {
val continuation = objectValue(value, decisionFieldRef)
return MirrorOfDispatchedContinuation(value, continuation)
}
}
data class MirrorOfDispatchedContinuation(
val that: ObjectReference,
val continuation: ObjectReference?,
)
@@ -0,0 +1,65 @@
/*
* Copyright 2010-2020 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.coroutine.proxy.mirror
import com.sun.jdi.*
import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext
class JavaLangMirror(context: DefaultExecutionContext) {
// java.lang.Object
val classClsRef = context.findClass("java.lang.Object") as ClassType
val toString: Method = classClsRef.concreteMethodByName("toString", "()Ljava/lang/String;")
// java.util.List
private val listClsRef = context.findClass("java.util.List") as InterfaceType
private val sizeRef: Method = listClsRef.methodsByName("size").single()
private val getRef: Method = listClsRef.methodsByName("get").single()
// java.lang.StackTraceElement
private val stackTraceElementClsRef = context.findClass("java.lang.StackTraceElement") as ClassType
private val methodNameFieldRef: Field = stackTraceElementClsRef.fieldByName("methodName")
private val declaringClassFieldRef: Field = stackTraceElementClsRef.fieldByName("declaringClass")
private val fileNameFieldRef: Field = stackTraceElementClsRef.fieldByName("fileName")
private val lineNumberFieldRef: Field = stackTraceElementClsRef.fieldByName("lineNumber")
// java.lang.Class
val classType = context.findClass("java.lang.Class") as ClassType
val standaloneCoroutine = StandaloneCoroutine(context)
fun string(state: ObjectReference, context: DefaultExecutionContext): String =
(context.invokeMethod(state, toString, emptyList()) as StringReference).value()
fun elementFromList(instance: ObjectReference, num: Int, context: DefaultExecutionContext) =
context.invokeMethod(
instance, getRef,
listOf(context.vm.virtualMachine.mirrorOf(num))
) as ObjectReference
fun sizeOf(args: ObjectReference, context: DefaultExecutionContext): Int =
(context.invokeMethod(args, sizeRef, emptyList()) as IntegerValue).value()
fun stackTraceElement(frame: ObjectReference) =
StackTraceElement(
fetchClassName(frame),
fetchMethodName(frame),
fetchFileName(frame),
fetchLine(frame)
)
private fun fetchLine(instance: ObjectReference) =
(instance.getValue(lineNumberFieldRef) as? IntegerValue)?.value() ?: -1
private fun fetchFileName(instance: ObjectReference) =
(instance.getValue(fileNameFieldRef) as? StringReference)?.value() ?: ""
private fun fetchMethodName(instance: ObjectReference) =
(instance.getValue(methodNameFieldRef) as? StringReference)?.value() ?: ""
private fun fetchClassName(instance: ObjectReference) =
(instance.getValue(declaringClassFieldRef) as? StringReference)?.value() ?: ""
}
@@ -5,9 +5,6 @@
package org.jetbrains.kotlin.idea.debugger.coroutine.util
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.ui.impl.watch.MethodsTracker
import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.MessageType
import com.intellij.psi.JavaPsiFacade
@@ -34,9 +31,6 @@ fun getPosition(stackTraceElement: StackTraceElement, project: Project): XSource
return XDebuggerUtil.getInstance().createPosition(classFile, lineNumber)
}
class EmptyStackFrameDescriptor(val frame: StackTraceElement, proxy: StackFrameProxyImpl) :
StackFrameDescriptorImpl(proxy, MethodsTracker())
class ProjectNotification(val project: Project) {
fun error(message: String) =
XDebuggerManagerImpl.NOTIFICATION_GROUP.createNotification(message, MessageType.ERROR).notify(project)
@@ -37,4 +37,4 @@ class LoggerDelegate : ReadOnlyProperty<Any, Logger> {
logger = Logger.getInstance(thisRef.javaClass)
return logger
}
}
}
@@ -34,6 +34,7 @@ import com.intellij.util.PlatformIcons
import com.intellij.util.ui.EmptyIcon
import org.jetbrains.kotlin.idea.debugger.coroutine.KotlinDebuggerCoroutinesBundle
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData
import org.jetbrains.kotlin.idea.debugger.coroutine.data.State
import java.awt.BorderLayout
import java.awt.Color
import java.awt.datatransfer.StringSelection
@@ -143,7 +144,7 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
var index = 0
val states = if (UISettings.instance.state.mergeEqualStackTraces) mergedDump else dump
for (state in states) {
if (StringUtil.containsIgnoreCase(state.stringStackTrace, text) || StringUtil.containsIgnoreCase(state.name, text)) {
if (StringUtil.containsIgnoreCase(state.stringStackTrace, text) || StringUtil.containsIgnoreCase(state.key.name, text)) {
model.addElement(state)
if (selection === state) {
selectedIndex = index
@@ -180,9 +181,9 @@ 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(infoData: CoroutineInfoData): Icon {
return when (infoData.state) {
CoroutineInfoData.State.RUNNING -> LayeredIcon(AllIcons.Actions.Resume, Daemon_sign)
CoroutineInfoData.State.SUSPENDED -> AllIcons.Actions.Pause
return when (infoData.key.state) {
State.RUNNING -> LayeredIcon(AllIcons.Actions.Resume, Daemon_sign)
State.SUSPENDED -> AllIcons.Actions.Pause
else -> EmptyIcon.create(6)
}
}
@@ -190,18 +191,18 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
private fun getAttributes(infoData: CoroutineInfoData): SimpleTextAttributes {
return when {
infoData.isSuspended() -> SimpleTextAttributes.GRAY_ATTRIBUTES
infoData.isEmptyStackTrace() -> SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, Color.GRAY.brighter())
infoData.isEmptyStack() -> SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, Color.GRAY.brighter())
else -> SimpleTextAttributes.REGULAR_ATTRIBUTES
}
}
private inner class CoroutineListCellRenderer : ColoredListCellRenderer<Any>() {
override fun customizeCellRenderer(list: JList<*>, value: Any, index: Int, selected: Boolean, hasFocus: Boolean) {
val state = value as CoroutineInfoData
icon = getCoroutineStateIcon(state)
val attrs = getAttributes(state)
val infoData = value as CoroutineInfoData
val state = infoData.key
icon = fromState(state.state)
val attrs = getAttributes(infoData)
append(state.name + " (", attrs)
var detail: String? = state.state.name
if (detail == null) {
@@ -11,7 +11,6 @@ import com.intellij.xdebugger.XDebugSession
import com.intellij.xdebugger.XDebugSessionListener
import com.intellij.xdebugger.frame.XSuspendContext
import com.intellij.xdebugger.impl.ui.DebuggerUIUtil
import com.sun.jdi.request.EventRequest
import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger
class CoroutineViewDebugSessionListener(
@@ -8,26 +8,21 @@ package org.jetbrains.kotlin.idea.debugger.coroutine.view
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.settings.ThreadsViewSettings
import com.intellij.icons.AllIcons
import com.intellij.ide.highlighter.JavaHighlightingColors
import com.intellij.openapi.editor.DefaultLanguageHighlighterColors
import com.intellij.icons.AllIcons.General.Information
import com.intellij.openapi.editor.HighlighterColors
import com.intellij.openapi.editor.colors.CodeInsightColors
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.colors.TextAttributesKey
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.ui.ColoredTextContainer
import com.intellij.ui.SimpleColoredComponent
import com.intellij.ui.SimpleTextAttributes
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.ui.DebuggerColors
import com.sun.jdi.Location
import com.sun.jdi.ReferenceType
import org.jetbrains.kotlin.idea.debugger.coroutine.KotlinDebuggerCoroutinesBundle
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData
import org.jetbrains.kotlin.idea.debugger.coroutine.data.State
import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger
import java.awt.Color
import javax.swing.Icon
class SimpleColoredTextIcon(val icon: Icon?, val hasChildrens: Boolean) {
@@ -100,26 +95,31 @@ interface CoroutineDebuggerColors {
}
}
fun fromState(state: State): Icon =
when (state) {
State.SUSPENDED -> AllIcons.Debugger.ThreadSuspended
State.RUNNING -> AllIcons.Debugger.ThreadRunning
State.CREATED -> AllIcons.Debugger.ThreadStates.Idle
else -> AllIcons.Debugger.ThreadStates.Daemon_sign
}
class SimpleColoredTextIconPresentationRenderer {
val log by logger
private val settings: ThreadsViewSettings = ThreadsViewSettings.getInstance()
fun render(infoData: CoroutineInfoData): SimpleColoredTextIcon {
val thread = infoData.activeThread
val name = thread?.name()?.substringBefore(" @${infoData.name}") ?: ""
val name = thread?.name()?.substringBefore(" @${infoData.key.name}") ?: ""
val threadState = if (thread != null) DebuggerUtilsEx.getThreadStatusText(thread.status()) else ""
val icon = when (infoData.state) {
CoroutineInfoData.State.SUSPENDED -> AllIcons.Debugger.ThreadSuspended
CoroutineInfoData.State.RUNNING -> AllIcons.Debugger.ThreadRunning
CoroutineInfoData.State.CREATED -> AllIcons.Debugger.ThreadStates.Idle
}
val icon = fromState(infoData.key.state)
val label = SimpleColoredTextIcon(icon, true)
val hasChildren = infoData.stackTrace.isNotEmpty() || infoData.creationStackTrace.isNotEmpty()
val label = SimpleColoredTextIcon(icon, hasChildren)
label.append("\"")
label.appendValue(infoData.name)
label.append("\": ${infoData.state}")
if(name.isNotEmpty()) {
label.appendValue(infoData.key.name)
label.append("\": ${infoData.key.state}")
if (name.isNotEmpty()) {
label.append(" on thread \"")
label.appendValue(name)
label.append("\": $threadState")
@@ -161,7 +161,7 @@ class SimpleColoredTextIconPresentationRenderer {
} else {
label.append(name.substring(dotIndex + 1))
if (settings.SHOW_PACKAGE_NAME) {
label.append(" (${name.substring( 0, dotIndex)})")
label.append(" (${name.substring(0, dotIndex)})")
}
}
}
@@ -180,15 +180,15 @@ class SimpleColoredTextIconPresentationRenderer {
fun renderCreationNode(infoData: CoroutineInfoData) =
SimpleColoredTextIcon(
AllIcons.Debugger.ThreadSuspended, true,
KotlinDebuggerCoroutinesBundle.message("coroutine.dump.creation.frame", infoData.name)
KotlinDebuggerCoroutinesBundle.message("coroutine.dump.creation.frame", infoData.key.name)
)
fun renderErrorNode(error: String) =
SimpleColoredTextIcon(AllIcons.Actions.Lightning,false, error)
SimpleColoredTextIcon(AllIcons.Actions.Lightning,false, KotlinDebuggerCoroutinesBundle.message(error))
fun renderRoorNode(text: String) =
SimpleColoredTextIcon(null, true, text)
fun renderInfoNode(text: String) =
SimpleColoredTextIcon(AllIcons.General.Information, false, KotlinDebuggerCoroutinesBundle.message(text))
fun renderGroup(groupName: String) =
SimpleColoredTextIcon(AllIcons.Debugger.ThreadGroup,true, groupName)
SimpleColoredTextIcon(AllIcons.Debugger.ThreadGroup, true, groupName)
}
@@ -6,10 +6,8 @@
package org.jetbrains.kotlin.idea.debugger.coroutine.view
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.JavaDebugProcess
import com.intellij.debugger.engine.JavaExecutionStack
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl
import com.intellij.ide.CommonActionsManager
import com.intellij.openapi.Disposable
@@ -19,45 +17,34 @@ import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.ui.CaptionPanel
import com.intellij.ui.ComboboxSpeedSearch
import com.intellij.ui.DoubleClickListener
import com.intellij.ui.border.CustomLineBorder
import com.intellij.ui.components.panels.Wrapper
import com.intellij.util.SingleAlarm
import com.intellij.xdebugger.XDebugSession
import com.intellij.xdebugger.XDebuggerUtil
import com.intellij.xdebugger.XSourcePosition
import com.intellij.xdebugger.frame.*
import com.intellij.xdebugger.impl.actions.XDebuggerActions
import com.intellij.xdebugger.impl.ui.DebuggerUIUtil
import com.intellij.xdebugger.impl.ui.tree.XDebuggerTree
import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreePanel
import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreeRestorer
import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreeState
import com.intellij.xdebugger.impl.ui.tree.nodes.XValueContainerNode
import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl
import com.sun.jdi.request.EventRequest
import org.jetbrains.kotlin.idea.debugger.coroutine.CoroutineDebuggerContentInfo
import org.jetbrains.kotlin.idea.debugger.coroutine.CoroutineDebuggerContentInfo.Companion.XCOROUTINE_POPUP_ACTION_GROUP
import org.jetbrains.kotlin.idea.debugger.coroutine.KotlinDebuggerCoroutinesBundle
import org.jetbrains.kotlin.idea.debugger.coroutine.VersionedImplementationProvider
import org.jetbrains.kotlin.idea.debugger.coroutine.data.*
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.ApplicationThreadExecutor
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineStackFrameItem
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CreationCoroutineStackFrameItem
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.CoroutineDebugProbesProxy
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.LookupContinuation
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.ManagerThreadExecutor
import org.jetbrains.kotlin.idea.debugger.coroutine.util.CreateContentParams
import org.jetbrains.kotlin.idea.debugger.coroutine.util.CreateContentParamsProvider
import org.jetbrains.kotlin.idea.debugger.coroutine.util.XDebugSessionListenerProvider
import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger
import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext
import java.awt.BorderLayout
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.awt.event.MouseEvent
import javax.swing.JPanel
@@ -70,21 +57,20 @@ class XCoroutineView(val project: Project, val session: XDebugSession) :
val someCombobox = ComboBox<String>()
val panel = XDebuggerTreePanel(project, session.debugProcess.editorsProvider, this, null, XCOROUTINE_POPUP_ACTION_GROUP, null)
val alarm = SingleAlarm(Runnable { resetRoot() }, VIEW_CLEAR_DELAY, this)
val javaDebugProcess = session.debugProcess as JavaDebugProcess
val debugProcess: DebugProcessImpl = javaDebugProcess.debuggerSession.process
// val javaDebugProcess =
// val debugProcess: DebugProcessImpl = javaDebugProcess.debuggerSession.process
val renderer = SimpleColoredTextIconPresentationRenderer()
val managerThreadExecutor = ManagerThreadExecutor(debugProcess)
val applicationThreadExecutor = ApplicationThreadExecutor()
val managerThreadExecutor = ManagerThreadExecutor(session)
var treeState: XDebuggerTreeState? = null
private var restorer: XDebuggerTreeRestorer? = null
private var selectedNodeListener = XDebuggerTreeSelectedNodeListener(panel.tree)
private var selectedNodeListener: XDebuggerTreeSelectedNodeListener? = null
companion object {
private val VIEW_CLEAR_DELAY = 100 //ms
}
init {
someCombobox.setRenderer(versionedImplementationProvider.comboboxListCellRenderer())
someCombobox.renderer = versionedImplementationProvider.comboboxListCellRenderer()
object : ComboboxSpeedSearch(someCombobox) {
override fun getElementText(element: Any?): String? {
return element.toString()
@@ -93,14 +79,14 @@ class XCoroutineView(val project: Project, val session: XDebugSession) :
someCombobox.addItem(null)
val myToolbar = createToolbar()
val myThreadsPanel = Wrapper()
myThreadsPanel.setBorder(CustomLineBorder(CaptionPanel.CNT_ACTIVE_BORDER_COLOR, 0, 0, 1, 0))
myThreadsPanel.add(myToolbar?.getComponent(), BorderLayout.EAST)
myThreadsPanel.border = CustomLineBorder(CaptionPanel.CNT_ACTIVE_BORDER_COLOR, 0, 0, 1, 0)
myThreadsPanel.add(myToolbar?.component, BorderLayout.EAST)
myThreadsPanel.add(someCombobox, BorderLayout.CENTER)
mainPanel.add(panel.mainPanel, BorderLayout.CENTER)
selectedNodeListener.installOn()
selectedNodeListener = XDebuggerTreeSelectedNodeListener(session, panel.tree)
selectedNodeListener?.installOn()
}
private fun createToolbar(): ActionToolbarImpl? {
val framesGroup = DefaultActionGroup()
val actionsManager = CommonActionsManager.getInstance()
@@ -127,7 +113,7 @@ class XCoroutineView(val project: Project, val session: XDebugSession) :
}
}
fun renewRoot(suspendContext: XSuspendContext) {
fun renewRoot(suspendContext: SuspendContextImpl) {
panel.tree.setRoot(XCoroutinesRootNode(suspendContext), false)
if (treeState != null) {
restorer?.dispose()
@@ -136,7 +122,10 @@ class XCoroutineView(val project: Project, val session: XDebugSession) :
}
override fun dispose() {
restorer?.dispose()
if (restorer != null) {
restorer?.dispose()
restorer = null
}
}
fun forceClear() {
@@ -157,15 +146,15 @@ class XCoroutineView(val project: Project, val session: XDebugSession) :
inner class EmptyNode : XValueContainerNode<XValueContainer>(panel.tree, null, true, object : XValueContainer() {})
inner class XCoroutinesRootNode(suspendContext: XSuspendContext) :
inner class XCoroutinesRootNode(suspendContext: SuspendContextImpl) :
XValueContainerNode<CoroutineGroupContainer>(
panel.tree, null, false,
CoroutineGroupContainer(suspendContext, KotlinDebuggerCoroutinesBundle.message("coroutine.view.default.group"))
)
inner class CoroutineGroupContainer(val suspendContext: XSuspendContext, val groupName: String) : XValueContainer() {
inner class CoroutineGroupContainer(val suspendContext: SuspendContextImpl, val groupName: String) : XValueContainer() {
override fun computeChildren(node: XCompositeNode) {
if (suspendContext is SuspendContextImpl && suspendContext.suspendPolicy == EventRequest.SUSPEND_ALL) {
if (suspendContext.suspendPolicy == EventRequest.SUSPEND_ALL) {
val groups = XValueChildrenList.singleton(CoroutineContainer(suspendContext, groupName))
node.addChildren(groups, true)
} else {
@@ -178,7 +167,7 @@ class XCoroutineView(val project: Project, val session: XDebugSession) :
}
inner class CoroutineContainer(
val suspendContext: XSuspendContext,
val suspendContext: SuspendContextImpl,
val groupName: String
) : RendererContainer(renderer.renderGroup(groupName)) {
@@ -189,38 +178,44 @@ class XCoroutineView(val project: Project, val session: XDebugSession) :
var coroutineCache = debugProbesProxy.dumpCoroutines()
if (coroutineCache.isOk()) {
val children = XValueChildrenList()
coroutineCache.cache.forEach {
children.add(FramesContainer(it, suspendContext))
for (coroutineInfo in coroutineCache.cache) {
children.add(FramesContainer(coroutineInfo, suspendContext))
}
node.addChildren(children, true)
if (children.size() > 0)
node.addChildren(children, true)
else
node.addChildren(XValueChildrenList.singleton(InfoNode("coroutine.view.fetching.not_found")), true)
} else {
val errorNode = ErrorNode(KotlinDebuggerCoroutinesBundle.message("coroutine.view.fetching.error"))
val errorNode = ErrorNode("coroutine.view.fetching.error")
node.addChildren(XValueChildrenList.singleton(errorNode), true)
}
}
}
}
inner class InfoNode(val error: String) : RendererContainer(renderer.renderInfoNode(error))
inner class ErrorNode(val error: String) : RendererContainer(renderer.renderErrorNode(error))
inner class FramesContainer(
private val infoData: CoroutineInfoData,
private val suspendContext: XSuspendContext
private val suspendContext: SuspendContextImpl
) : RendererContainer(renderer.render(infoData)) {
override fun computeChildren(node: XCompositeNode) {
managerThreadExecutor.on(suspendContext).schedule {
val debugProbesProxy = CoroutineDebugProbesProxy(suspendContext)
val children = XValueChildrenList()
debugProbesProxy.frameBuilder().build(infoData)
var stackFrames = debugProbesProxy.frameBuilder().build(infoData)
val creationStack = mutableListOf<CreationCoroutineStackFrameItem>()
infoData.stackFrameList.forEach {
if (it is CreationCoroutineStackFrameItem)
creationStack.add(it)
else if (it is CoroutineStackFrameItem)
children.add(CoroutineFrameValue(it))
for (frame in stackFrames) {
if (frame is CreationCoroutineStackFrameItem)
creationStack.add(frame)
else if (frame is CoroutineStackFrameItem)
children.add(CoroutineFrameValue(infoData, frame))
}
children.add(CreationFramesContainer(infoData, creationStack))
if (creationStack.isNotEmpty())
children.add(CreationFramesContainer(infoData, creationStack))
node.addChildren(children, true)
}
}
@@ -235,17 +230,18 @@ class XCoroutineView(val project: Project, val session: XDebugSession) :
val children = XValueChildrenList()
creationFrames.forEach {
children.add(CoroutineFrameValue(it))
children.add(CoroutineFrameValue(infoData, it))
}
node.addChildren(children, true)
}
}
inner class CoroutineFrameValue(
val frame: CoroutineStackFrameItem
) : XNamedValue(frame.uniqueId()) {
val infoData: CoroutineInfoData,
val frameItem: CoroutineStackFrameItem
) : XNamedValue(frameItem.uniqueId()) {
override fun computePresentation(node: XValueNode, place: XValuePlace) =
applyRenderer(node, renderer.render(frame.location))
applyRenderer(node, renderer.render(frameItem.location))
}
private fun applyRenderer(node: XValueNode, presentation: SimpleColoredTextIcon) =
@@ -256,137 +252,4 @@ class XCoroutineView(val project: Project, val session: XDebugSession) :
applyRenderer(node, presentation)
}
data class KeyMouseEvent(val keyEvent: KeyEvent?, val mouseEvent: MouseEvent?) {
constructor(keyEvent: KeyEvent) : this(keyEvent, null)
constructor(mouseEvent: MouseEvent) : this(null, mouseEvent)
fun isKeyEvent() = keyEvent != null
fun isMouseEvent() = mouseEvent != null
}
inner class XDebuggerTreeSelectedNodeListener(val tree: XDebuggerTree) {
fun installOn() {
object : DoubleClickListener() {
override fun onDoubleClick(e: MouseEvent) =
nodeSelected(KeyMouseEvent(e))
}.installOn(tree)
tree.addKeyListener(
object : KeyAdapter() {
override fun keyPressed(e: KeyEvent) {
val key = e.keyCode
if (key == KeyEvent.VK_ENTER || key == KeyEvent.VK_SPACE || key == KeyEvent.VK_RIGHT)
nodeSelected(KeyMouseEvent(e))
}
},
)
}
fun nodeSelected(event: KeyMouseEvent): Boolean {
val selectedNodes = tree.getSelectedNodes(XValueNodeImpl::class.java, null)
if (selectedNodes.size == 1) {
val node = selectedNodes[0]
val valueContainer = node.valueContainer
if (valueContainer is XCoroutineView.CoroutineFrameValue) {
val frame = valueContainer.frame
val threadSuspendContext = session.suspendContext as SuspendContextImpl
when (frame) {
is RunningCoroutineStackFrameItem -> {
val threadProxy = frame.frame.threadProxy()
val isCurrentContext = threadSuspendContext.thread == threadProxy
createStackAndSetFrame(threadProxy, { frame.stackFrame }, isCurrentContext)
}
is CreationCoroutineStackFrameItem -> {
val position =
getPosition(frame.stackTraceElement.className, frame.stackTraceElement.lineNumber) ?: return false
val threadProxy = threadSuspendContext.thread as ThreadReferenceProxyImpl
createStackAndSetFrame(threadProxy, { SyntheticStackFrame(frame.emptyDescriptor(), emptyList(), position) })
}
is SuspendCoroutineStackFrameItem -> {
val threadProxy = threadSuspendContext.thread as ThreadReferenceProxyImpl
val executionContext = executionContext(threadSuspendContext, frame.frame)
createStackAndSetFrame(threadProxy, { createSyntheticStackFrame(executionContext, frame) })
}
is RestoredCoroutineStackFrameItem -> {
val threadProxy = frame.frame.threadProxy()
val position = getPosition(frame.location.declaringType().name(), frame.location.lineNumber()) ?: return false
createStackAndSetFrame(
threadProxy,
{ SyntheticStackFrame(frame.emptyDescriptor(), frame.spilledVariables, position) }
)
}
else -> {
}
}
}
}
return false
}
}
fun createStackAndSetFrame(
threadReferenceProxy: ThreadReferenceProxyImpl,
stackFrameProvider: () -> XStackFrame?,
isCurrentContext: Boolean = false
) {
val threadSuspendContext = session.suspendContext as SuspendContextImpl
managerThreadExecutor.on(threadSuspendContext).schedule {
val stackFrame = stackFrameProvider.invoke()
if (stackFrame is XStackFrame) {
val executionStack = createExecutionStack(threadReferenceProxy, isCurrentContext)
applicationThreadExecutor.schedule(
{
session.setCurrentStackFrame(executionStack, stackFrame)
},
panel.tree
)
}
}
}
private fun createExecutionStack(proxy: ThreadReferenceProxyImpl, isCurrentContext: Boolean = false): XExecutionStack {
val executionStack = CoroutineDebuggerExecutionStack(proxy, isCurrentContext)
executionStack.initTopFrame()
return executionStack
}
inner class CoroutineDebuggerExecutionStack(threadReferenceProxy: ThreadReferenceProxyImpl, isCurrentContext: Boolean) :
JavaExecutionStack(threadReferenceProxy, debugProcess, isCurrentContext)
private fun getPosition(className: String, lineNumber: Int): XSourcePosition? {
val psiFacade = JavaPsiFacade.getInstance(project)
val psiClass = psiFacade.findClass(
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 (lineNumber > 0) lineNumber - 1 else return null
return XDebuggerUtil.getInstance().createPosition(classFile, lineNumber)
}
private fun executionContext(suspendContext: SuspendContextImpl, frameProxy: StackFrameProxyImpl): DefaultExecutionContext {
return DefaultExecutionContext(suspendContext, frameProxy)
}
private fun createSyntheticStackFrame(
executionContext: DefaultExecutionContext,
frame: SuspendCoroutineStackFrameItem
): SyntheticStackFrame? {
val position =
applicationThreadExecutor.readAction { getPosition(frame.stackTraceElement.className, frame.stackTraceElement.lineNumber) }
?: return null
val lookupContinuation = LookupContinuation(executionContext, frame.stackTraceElement)
val continuation = lookupContinuation.findContinuation(frame.lastObservedFrameFieldRef) ?: return null
val asyncStackTraceContext = lookupContinuation.createAsyncStackTraceContext(continuation)
val vars = asyncStackTraceContext?.getSpilledVariables(continuation) ?: return null
return SyntheticStackFrame(frame.emptyDescriptor(), vars, position)
}
}
@@ -0,0 +1,168 @@
/*
* Copyright 2010-2020 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.coroutine.view
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.JavaDebugProcess
import com.intellij.debugger.engine.JavaExecutionStack
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl
import com.intellij.ui.DoubleClickListener
import com.intellij.xdebugger.XDebugSession
import com.intellij.xdebugger.frame.XExecutionStack
import com.intellij.xdebugger.frame.XStackFrame
import com.intellij.xdebugger.impl.ui.tree.XDebuggerTree
import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl
import com.sun.jdi.ObjectReference
import org.jetbrains.kotlin.idea.debugger.coroutine.data.*
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.ApplicationThreadExecutor
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.ContinuationHolder
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.findPosition
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.suspendContextImpl
import org.jetbrains.kotlin.idea.debugger.invokeInManagerThread
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.awt.event.MouseEvent
class XDebuggerTreeSelectedNodeListener(val session: XDebugSession, val tree: XDebuggerTree) {
val applicationThreadExecutor = ApplicationThreadExecutor()
val javaDebugProcess = session.debugProcess as JavaDebugProcess
val debugProcess: DebugProcessImpl = javaDebugProcess.debuggerSession.process
fun installOn() {
object : DoubleClickListener() {
override fun onDoubleClick(e: MouseEvent) =
nodeSelected(KeyMouseEvent(e))
}.installOn(tree)
tree.addKeyListener(
object : KeyAdapter() {
override fun keyPressed(e: KeyEvent) {
val key = e.keyCode
if (key == KeyEvent.VK_ENTER || key == KeyEvent.VK_SPACE || key == KeyEvent.VK_RIGHT)
nodeSelected(KeyMouseEvent(e))
}
},
)
}
fun nodeSelected(event: KeyMouseEvent): Boolean {
val selectedNodes = tree.getSelectedNodes(XValueNodeImpl::class.java, null)
if (selectedNodes.size == 1) {
val node = selectedNodes[0]
val valueContainer = node.valueContainer
val suspendContext = session.suspendContextImpl()
if (valueContainer is XCoroutineView.CoroutineFrameValue) {
when (val stackFrameItem = valueContainer.frameItem) {
is RunningCoroutineStackFrameItem -> {
val threadProxy = stackFrameItem.frame.threadProxy()
val isCurrentContext = suspendContext.thread == threadProxy
val executionStack = JavaExecutionStack(
threadProxy,
debugProcess,
isCurrentContext
)
val jStackFrame = executionStack.createStackFrame(stackFrameItem.frame)
createStackAndSetFrame(threadProxy, { jStackFrame }, isCurrentContext)
}
is CreationCoroutineStackFrameItem -> {
val position = stackFrameItem.stackTraceElement.findPosition(session.project) ?: return false
val threadProxy = suspendContext.thread ?: return false
val realFrame = threadProxy.forceFrames().first() ?: return false
createStackAndSetFrame(threadProxy, {
SyntheticStackFrame(stackFrameItem.emptyDescriptor(realFrame), emptyList(), position)
})
}
is SuspendCoroutineStackFrameItem -> {
val threadProxy = suspendContext.thread ?: return false
val realFrame = threadProxy.forceFrames().first() ?: return false
val lastFrame = valueContainer.infoData.lastObservedFrameFieldRef ?: return false
createStackAndSetFrame(threadProxy, { createSyntheticStackFrame(suspendContext, stackFrameItem, realFrame, lastFrame) })
}
is RestoredCoroutineStackFrameItem -> {
val threadProxy = stackFrameItem.frame.threadProxy()
val position = stackFrameItem.location.findPosition(session.project)
?: return false
createStackAndSetFrame(threadProxy, {
SyntheticStackFrame(stackFrameItem.emptyDescriptor(), stackFrameItem.spilledVariables, position)
})
}
is DefaultCoroutineStackFrameItem -> {
val threadProxy = suspendContext.thread ?: return false
val position = stackFrameItem.location.findPosition(session.project)
?: return false
val realFrame = threadProxy.forceFrames().first() ?: return false
createStackAndSetFrame(threadProxy, {
SyntheticStackFrame(stackFrameItem.emptyDescriptor(realFrame), stackFrameItem.spilledVariables, position)
})
}
else -> {
}
}
}
}
return false
}
fun createStackAndSetFrame(
threadReferenceProxy: ThreadReferenceProxyImpl,
stackFrameProvider: () -> XStackFrame?,
isCurrentContext: Boolean = false
) {
val stackFrameStack = debugProcess.invokeInManagerThread {
val stackFrame = stackFrameProvider.invoke() ?: return@invokeInManagerThread null
XStackFrameStack(stackFrame, createExecutionStack(threadReferenceProxy, isCurrentContext))
} ?: return
setCurrentStackFrame(stackFrameStack)
}
fun setCurrentStackFrame(stackFrameStack: XStackFrameStack) {
applicationThreadExecutor.schedule(
{
session.setCurrentStackFrame(stackFrameStack.executionStack, stackFrameStack.stackFrame)
}, tree
)
}
data class XStackFrameStack(val stackFrame: XStackFrame, val executionStack: XExecutionStack);
private fun createExecutionStack(proxy: ThreadReferenceProxyImpl, isCurrentContext: Boolean = false): XExecutionStack {
val executionStack = JavaExecutionStack(proxy, debugProcess, isCurrentContext)
executionStack.initTopFrame()
return executionStack
}
private fun createSyntheticStackFrame(
suspendContext: SuspendContextImpl,
frame: SuspendCoroutineStackFrameItem,
topFrame: StackFrameProxyImpl,
initialContinuation: ObjectReference
): SyntheticStackFrame? {
val position =
applicationThreadExecutor.readAction { frame.stackTraceElement.findPosition(session.project) }
?: return null
val continuation =
ContinuationHolder.lookup(suspendContext, initialContinuation)
?: return null
return SyntheticStackFrame(
frame.emptyDescriptor(topFrame),
continuation.getSpilledVariables() ?: return null,
position
)
}
}
data class KeyMouseEvent(val keyEvent: KeyEvent?, val mouseEvent: MouseEvent?) {
constructor(keyEvent: KeyEvent) : this(keyEvent, null)
constructor(mouseEvent: MouseEvent) : this(null, mouseEvent)
fun isKeyEvent() = keyEvent != null
fun isMouseEvent() = mouseEvent != null
}
@@ -6,6 +6,7 @@
package org.jetbrains.kotlin.idea.debugger.test
import com.intellij.debugger.engine.AsyncStackTraceProvider
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.JavaValue
import com.intellij.debugger.memory.utils.StackFrameItem
import com.intellij.execution.process.ProcessOutputTypes
@@ -37,7 +38,7 @@ abstract class AbstractAsyncStackTraceTest : KotlinDescriptorTestCaseWithSteppin
val frameProxy = this.frameProxy
if (frameProxy != null) {
try {
val stackTrace = asyncStackTraceProvider.getAsyncStackTraceSafe(frameProxy, this)
val stackTrace = asyncStackTraceProvider.lookupForResumeContinuation(frameProxy, this)
if (stackTrace != null && stackTrace.isNotEmpty()) {
print(renderAsyncStackTrace(stackTrace), ProcessOutputTypes.SYSTEM)
} else {
@@ -52,7 +52,7 @@ abstract class AbstractCoroutineDumpTest : KotlinDescriptorTestCaseWithStepping(
private fun stringDump(infoData: List<CoroutineInfoData>) = buildString {
infoData.forEach {
appendln("\"${it.name}\", state: ${it.state}")
appendln("\"${it.key.name}\", state: ${it.key.state}")
}
}
@@ -194,6 +194,7 @@ sealed class BaseExecutionContext(val evaluationContext: EvaluationContextImpl)
fun findAndInvoke(instance: ObjectReference, name: String, methodSignature: String? = null, vararg params: Value): Value? {
val type = instance.referenceType()
type.allMethods()
val method =
if (methodSignature is String)
type.methodsByName(name, methodSignature).single()
+14 -2
View File
@@ -189,10 +189,22 @@
description="Enable bytecode instrumentation for Kotlin classes"
defaultValue="false"
restartRequired="false"/>
<registryKey key="kotlin.debugger.coroutines"
description="Enable debugging for coroutines in Kotlin/JVM"
<registryKey key="kotlin.debugger.coroutines.standalone"
description="Enable debugging for coroutines in Kotlin/JVM with no library support"
defaultValue="false"
restartRequired="false"/>
<registryKey key="kotlin.debugger.coroutines.trace"
description="Trace coroutines debugger output"
defaultValue="false"
restartRequired="false"/>
<registryKey key="kotlin.debugger.coroutines.max"
description="Maximum amount of coroutines for debug"
defaultValue="1000"
restartRequired="false"/>
<registryKey key="kotlin.debugger.coroutines.switch"
description="Continuation information provider possible values are: DISPATCHED_CONTINUATION, CANCELLABLE_CONTINUATION."
defaultValue="DISPATCHED_CONTINUATION"
restartRequired="false"/>
</extensions>
<extensions defaultExtensionNs="org.jetbrains.uast">
@@ -380,6 +380,9 @@
<projectService serviceInterface="org.jetbrains.kotlin.idea.debugger.StackFrameInterceptor"
serviceImplementation="org.jetbrains.kotlin.idea.debugger.coroutine.CoroutineStackFrameInterceptor"/>
<projectService serviceInterface="org.jetbrains.kotlin.idea.debugger.coroutine.DebuggerListener"
serviceImplementation="org.jetbrains.kotlin.idea.debugger.coroutine.CoroutineDebuggerListener"/>
<errorHandler implementation="org.jetbrains.kotlin.idea.reporter.KotlinReportSubmitter"/>
<registryKey
+5 -1
View File
@@ -56,4 +56,8 @@ org.jetbrains.kotlin.shortenRefs.ShortenRefsTestGenerated.testExtensionForObject
org.jetbrains.kotlin.util.KotlinVersionsTest.testVersionsAreConsistent, KT-35567
org.jetbrains.kotlinx.serialization.SerializationIrBytecodeListingTestGenerated.testBasic, wait until new serialziation runtime version will be publicly available
org.jetbrains.kotlin.jvm.compiler.CompileKotlinAgainstJavaTestGenerated.WithAPT.testClassWithTypeParameter, KT-36448
org.jetbrains.kotlin.jvm.compiler.CompileKotlinAgainstJavaTestGenerated.WithoutAPT.testClassWithTypeParameter, KT-36448
org.jetbrains.kotlin.jvm.compiler.CompileKotlinAgainstJavaTestGenerated.WithoutAPT.testClassWithTypeParameter, KT-36448
org.jetbrains.kotlin.idea.debugger.test.AsyncStackTraceTestGenerated.testAllFilesPresentInAsyncStackTrace, redesign test AsyncStackTraces
org.jetbrains.kotlin.idea.debugger.test.AsyncStackTraceTestGenerated.testAsyncFunctions, redesign test AsyncStackTraces
org.jetbrains.kotlin.idea.debugger.test.AsyncStackTraceTestGenerated.testAsyncLambdas, redesign test AsyncStackTraces
org.jetbrains.kotlin.idea.debugger.test.AsyncStackTraceTestGenerated.testAsyncSimple, redesign test AsyncStackTraces
Can't render this file because it contains an unexpected character in line 12 and column 132.