Debugger: Fix for coroutine debugger after review

This commit is contained in:
Aleksandr Prokopyev
2019-10-01 11:57:02 +03:00
committed by Vladimir Ilmov
parent 5975251a32
commit 12313fa5aa
9 changed files with 89 additions and 105 deletions
@@ -25,7 +25,7 @@ import com.intellij.openapi.util.registry.Registry
import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.text.DateFormatUtil import com.intellij.util.text.DateFormatUtil
import com.intellij.xdebugger.impl.XDebuggerManagerImpl import com.intellij.xdebugger.impl.XDebuggerManagerImpl
import org.jetbrains.kotlin.idea.debugger.evaluate.createExecutionContext import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
@Suppress("ComponentNotRegistered") @Suppress("ComponentNotRegistered")
class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate { class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate {
@@ -39,14 +39,16 @@ class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate {
val process = context.debugProcess ?: return val process = context.debugProcess ?: return
process.managerThread.schedule(object : SuspendContextCommandImpl(context.suspendContext) { process.managerThread.schedule(object : SuspendContextCommandImpl(context.suspendContext) {
override fun contextAction() { override fun contextAction() {
val execContext = context.createExecutionContext() ?: return val evalContext = context.createEvaluationContext()
val frameProxy = evalContext?.frameProxy ?: return
val execContext = ExecutionContext(evalContext, frameProxy)
val states = CoroutinesDebugProbesProxy.dumpCoroutines(execContext) val states = CoroutinesDebugProbesProxy.dumpCoroutines(execContext)
if (states.isLeft) { if (states.isLeft) {
logger.warn(states.left) logger.warn(states.left)
XDebuggerManagerImpl.NOTIFICATION_GROUP XDebuggerManagerImpl.NOTIFICATION_GROUP
.createNotification( .createNotification(
"Coroutine dump failed. See log", "Coroutine dump failed. See log",
MessageType.ERROR MessageType.WARNING
).notify(project) ).notify(project)
return return
} }
@@ -80,7 +80,7 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
} }
} }
exporterToTextFile = createToFileExporter(project, dump) exporterToTextFile = MyToFileExporter(project, dump)
val filterAction = FilterAction().apply { val filterAction = FilterAction().apply {
registerCustomShortcutSet( registerCustomShortcutSet(
@@ -92,7 +92,7 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
add(filterAction) add(filterAction)
add(CopyToClipboardAction(dump, project)) add(CopyToClipboardAction(dump, project))
add(ActionManager.getInstance().getAction(IdeActions.ACTION_EXPORT_TO_TEXT_FILE)) add(ActionManager.getInstance().getAction(IdeActions.ACTION_EXPORT_TO_TEXT_FILE))
add(MergeStacktracesAction()) add(MergeStackTracesAction())
} }
add( add(
ActionManager.getInstance() ActionManager.getInstance()
@@ -139,7 +139,6 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
val states = if (UISettings.instance.state.mergeEqualStackTraces) mergedDump else dump val states = if (UISettings.instance.state.mergeEqualStackTraces) mergedDump else dump
for (state in states) { for (state in states) {
if (StringUtil.containsIgnoreCase(state.stringStackTrace, text) || StringUtil.containsIgnoreCase(state.name, text)) { if (StringUtil.containsIgnoreCase(state.stringStackTrace, text) || StringUtil.containsIgnoreCase(state.name, text)) {
model.addElement(state) model.addElement(state)
if (selection === state) { if (selection === state) {
selectedIndex = index selectedIndex = index
@@ -211,9 +210,11 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
} }
} }
private inner class FilterAction : private inner class FilterAction : ToggleAction(
ToggleAction("Filter", "Show only threads containing a specific string", AllIcons.General.Filter), "Filter",
DumbAware { "Show only coroutines containing a specific string",
AllIcons.General.Filter
), DumbAware {
override fun isSelected(e: AnActionEvent): Boolean { override fun isSelected(e: AnActionEvent): Boolean {
return filterPanel.isVisible return filterPanel.isVisible
@@ -229,14 +230,11 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
} }
} }
@Suppress("DEPRECATION") private inner class MergeStackTracesAction : ToggleAction(
private inner class MergeStacktracesAction : "Merge Identical Stacktraces",
ToggleAction( "Group coroutines with identical stacktraces",
"Merge Identical Stacktraces", AllIcons.Actions.Collapseall
"Group coroutines with identical stacktraces", ), DumbAware {
AllIcons.General.CollapseAll
),
DumbAware {
override fun isSelected(e: AnActionEvent): Boolean { override fun isSelected(e: AnActionEvent): Boolean {
return UISettings.instance.state.mergeEqualStackTraces return UISettings.instance.state.mergeEqualStackTraces
@@ -268,32 +266,19 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
private val group = NotificationGroup.toolWindowGroup("Analyze coroutine dump", ToolWindowId.RUN, false) private val group = NotificationGroup.toolWindowGroup("Analyze coroutine dump", ToolWindowId.RUN, false)
} }
private fun createToFileExporter(project: Project, states: List<CoroutineState>): ExporterToTextFile { private class MyToFileExporter(
return MyToFileExporter(project, states) private val myProject: Project,
} private val states: List<CoroutineState>
) : ExporterToTextFile {
private class MyToFileExporter(private val myProject: Project, private val states: List<CoroutineState>) : override fun getReportText() = buildString {
ExporterToTextFile { for (state in states)
append(state.stringStackTrace).append("\n\n")
override fun getReportText(): String {
val sb = StringBuilder()
for (state in states) {
sb.append(state.stringStackTrace).append("\n\n")
}
return sb.toString()
} }
@Suppress("DEPRECATION") override fun getDefaultFilePath() = (myProject.basePath ?: "") + File.separator + defaultReportFileName
override fun getDefaultFilePath(): String {
val baseDir = myProject.baseDir
return if (baseDir != null) {
baseDir.presentableUrl + File.separator + defaultReportFileName
} else ""
}
override fun canExport(): Boolean { override fun canExport() = states.isNotEmpty()
return states.isNotEmpty()
}
private val defaultReportFileName = "coroutines_report.txt" private val defaultReportFileName = "coroutines_report.txt"
} }
@@ -61,13 +61,13 @@ class CoroutineState(
val trace = context.invokeMethod(continuation, getTrace, emptyList()) as? ObjectReference val trace = context.invokeMethod(continuation, getTrace, emptyList()) as? ObjectReference
if (trace != null) if (trace != null)
(context.invokeMethod(trace, getClassName, emptyList()) as StringReference).value() (context.invokeMethod(trace, getClassName, emptyList()) as StringReference).value()
else "" else null
} }
val lineNumber = { val lineNumber = {
val trace = context.invokeMethod(continuation, getTrace, emptyList()) as? ObjectReference val trace = context.invokeMethod(continuation, getTrace, emptyList()) as? ObjectReference
if (trace != null) if (trace != null)
(context.invokeMethod(trace, getLineNumber, emptyList()) as IntegerValue).value() (context.invokeMethod(trace, getLineNumber, emptyList()) as IntegerValue).value()
else -239 // invalid line number (but well-educated) else null
} }
while (continuation.type().isSubtype(baseType) while (continuation.type().isSubtype(baseType)
@@ -40,7 +40,7 @@ class CoroutinesDebugConfigurationExtension : RunConfigurationExtension() {
private var Project.listenerCreated: Boolean? by UserDataProperty(Key.create("COROUTINES_DEBUG_TAB_CREATE_LISTENER")) private var Project.listenerCreated: Boolean? by UserDataProperty(Key.create("COROUTINES_DEBUG_TAB_CREATE_LISTENER"))
override fun isApplicableFor(configuration: RunConfigurationBase<*>): Boolean { override fun isApplicableFor(configuration: RunConfigurationBase<*>): Boolean {
return Registry.`is`("kotlin.debugger" + ".coroutines") return Registry.`is`("kotlin.debugger.coroutines")
} }
override fun <T : RunConfigurationBase<*>?> updateJavaParameters( override fun <T : RunConfigurationBase<*>?> updateJavaParameters(
@@ -48,7 +48,7 @@ class CoroutinesDebugConfigurationExtension : RunConfigurationExtension() {
params: JavaParameters?, params: JavaParameters?,
runnerSettings: RunnerSettings? runnerSettings: RunnerSettings?
) { ) {
if (!Registry.`is`("kotlin.debugger" + ".coroutines")) return if (!Registry.`is`("kotlin.debugger.coroutines")) return
if (runnerSettings is DebuggingRunnerData && params != null if (runnerSettings is DebuggingRunnerData && params != null
&& params.classPath != null && params.classPath != null
&& params.classPath.pathList.isNotEmpty() && params.classPath.pathList.isNotEmpty()
@@ -78,8 +78,9 @@ class CoroutinesDebugConfigurationExtension : RunConfigurationExtension() {
override fun processStarted(debugProcess: XDebugProcess) { override fun processStarted(debugProcess: XDebugProcess) {
DebuggerInvocationUtil.swingInvokeLater(project) { DebuggerInvocationUtil.swingInvokeLater(project) {
val session = DebuggerManagerEx.getInstanceEx(project).context.debuggerSession val session = DebuggerManagerEx.getInstanceEx(project).context.debuggerSession
if (session != null) val ui = session?.xDebugSession?.ui
registerCoroutinesPanel(session.xDebugSession?.ui ?: return@swingInvokeLater, session) if (ui != null)
registerCoroutinesPanel(ui, session)
} }
} }
@@ -16,6 +16,7 @@ object CoroutinesDebugProbesProxy {
private const val DEBUG_PACKAGE = "kotlinx.coroutines.debug" private const val DEBUG_PACKAGE = "kotlinx.coroutines.debug"
private var DebugProcess.references by UserDataProperty(Key.create<ProcessReferences>("COROUTINES_DEBUG_REFERENCES")) private var DebugProcess.references by UserDataProperty(Key.create<ProcessReferences>("COROUTINES_DEBUG_REFERENCES"))
@Synchronized
@Suppress("unused") @Suppress("unused")
fun install(context: ExecutionContext) { fun install(context: ExecutionContext) {
val debugProbes = context.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType val debugProbes = context.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType
@@ -24,6 +25,7 @@ object CoroutinesDebugProbesProxy {
context.invokeMethod(instance, install, emptyList()) context.invokeMethod(instance, install, emptyList())
} }
@Synchronized
@Suppress("unused") @Suppress("unused")
fun uninstall(context: ExecutionContext) { fun uninstall(context: ExecutionContext) {
val debugProbes = context.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType val debugProbes = context.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType
@@ -36,6 +38,7 @@ object CoroutinesDebugProbesProxy {
* Invokes DebugProbes from debugged process's classpath and returns states of coroutines * Invokes DebugProbes from debugged process's classpath and returns states of coroutines
* Should be invoked on debugger manager thread * Should be invoked on debugger manager thread
*/ */
@Synchronized
fun dumpCoroutines(context: ExecutionContext): Either<Throwable, List<CoroutineState>> { fun dumpCoroutines(context: ExecutionContext): Either<Throwable, List<CoroutineState>> {
try { try {
if (context.debugProcess.references == null) { if (context.debugProcess.references == null) {
@@ -50,6 +53,7 @@ object CoroutinesDebugProbesProxy {
return Either.right(List(size) { return Either.right(List(size) {
val index = context.vm.mirrorOf(it) val index = context.vm.mirrorOf(it)
// `List<CoroutineInfo>.get(index)`
val elem = context.invokeMethod(infoList, refs.getElement, listOf(index)) as ObjectReference val elem = context.invokeMethod(infoList, refs.getElement, listOf(index)) as ObjectReference
val name = getName(context, elem, refs) val name = getName(context, elem, refs)
val state = getState(context, elem, refs) val state = getState(context, elem, refs)
@@ -73,7 +77,11 @@ object CoroutinesDebugProbesProxy {
refs: ProcessReferences refs: ProcessReferences
): String { ): String {
// equals to `coroutineInfo.context.get(CoroutineName).name` // equals to `coroutineInfo.context.get(CoroutineName).name`
val coroutineContextInst = context.invokeMethod(info, refs.getContext, emptyList()) as ObjectReference val coroutineContextInst = context.invokeMethod(
info,
refs.getContext,
emptyList()
) as? ObjectReference ?: throw IllegalArgumentException("Coroutine context must not be null")
val coroutineName = context.invokeMethod( val coroutineName = context.invokeMethod(
coroutineContextInst, coroutineContextInst,
refs.getContextElement, listOf(refs.nameKey) refs.getContextElement, listOf(refs.nameKey)
@@ -81,7 +89,8 @@ object CoroutinesDebugProbesProxy {
// If the coroutine doesn't have a given name, CoroutineContext.get(CoroutineName) returns null // If the coroutine doesn't have a given name, CoroutineContext.get(CoroutineName) returns null
val name = if (coroutineName != null) (context.invokeMethod( val name = if (coroutineName != null) (context.invokeMethod(
coroutineName, coroutineName,
refs.getName, emptyList() refs.getName,
emptyList()
) as StringReferenceImpl).value() else "coroutine" ) as StringReferenceImpl).value() else "coroutine"
val id = (info.getValue(refs.idField) as LongValue).value() val id = (info.getValue(refs.idField) as LongValue).value()
return "$name#$id" return "$name#$id"
@@ -92,7 +101,7 @@ object CoroutinesDebugProbesProxy {
info: ObjectReference, // CoroutineInfo instance info: ObjectReference, // CoroutineInfo instance
refs: ProcessReferences refs: ProcessReferences
): String { ): String {
// equals to stringState = coroutineInfo.state.toString() // equals to `stringState = coroutineInfo.state.toString()`
val state = context.invokeMethod(info, refs.getState, emptyList()) as ObjectReference val state = context.invokeMethod(info, refs.getState, emptyList()) as ObjectReference
return (context.invokeMethod(state, refs.toString, emptyList()) as StringReferenceImpl).value() return (context.invokeMethod(state, refs.toString, emptyList()) as StringReferenceImpl).value()
} }
@@ -100,7 +109,7 @@ object CoroutinesDebugProbesProxy {
private fun getLastObservedThread( private fun getLastObservedThread(
info: ObjectReference, // CoroutineInfo instance info: ObjectReference, // CoroutineInfo instance
threadRef: Field // reference to lastObservedThread threadRef: Field // reference to lastObservedThread
): ThreadReference? = info.getValue(threadRef) as ThreadReference? ): ThreadReference? = info.getValue(threadRef) as? ThreadReference
/** /**
* Returns list of stackTraceElements for the given CoroutineInfo's [ObjectReference] * Returns list of stackTraceElements for the given CoroutineInfo's [ObjectReference]
@@ -124,14 +133,12 @@ object CoroutinesDebugProbesProxy {
listOf(context.vm.virtualMachine.mirrorOf(it)) listOf(context.vm.virtualMachine.mirrorOf(it))
) as ObjectReference ) as ObjectReference
val clazz = (frame.getValue(refs.className) as StringReference).value() val clazz = (frame.getValue(refs.className) as StringReference).value()
// if (clazz.contains("$DEBUG_PACKAGE.DebugProbes")) break // cut off debug intrinsic stacktrace
list.add( list.add(
0, // add in the beginning 0, // add in the beginning
StackTraceElement( StackTraceElement(
clazz, clazz,
(frame.getValue(refs.methodName) as StringReference).value(), (frame.getValue(refs.methodName) as? StringReference)?.value(),
(frame.getValue(refs.fileName) as StringReference?)?.value(), (frame.getValue(refs.fileName) as? StringReference)?.value(),
(frame.getValue(refs.line) as IntegerValue).value() (frame.getValue(refs.line) as IntegerValue).value()
) )
) )
@@ -165,7 +172,7 @@ object CoroutinesDebugProbesProxy {
val getName: Method = coroutineName.methodsByName("getName").single() val getName: Method = coroutineName.methodsByName("getName").single()
val nameKey = coroutineName.getValue(coroutineName.fieldByName("Key")) as ObjectReference val nameKey = coroutineName.getValue(coroutineName.fieldByName("Key")) as ObjectReference
val toString: Method = (context.findClass("java.lang.Object") as ClassType) val toString: Method = (context.findClass("java.lang.Object") as ClassType)
.methodsByName("toString").single() .concreteMethodByName("toString", "()Ljava/lang/String;")
val threadRef: Field = info.fieldByName("lastObservedThread") val threadRef: Field = info.fieldByName("lastObservedThread")
val continuation: Field = info.fieldByName("lastObservedFrame") val continuation: Field = info.fieldByName("lastObservedFrame")
@@ -41,7 +41,6 @@ import com.sun.jdi.ClassType
import javaslang.control.Either import javaslang.control.Either
import org.jetbrains.kotlin.idea.debugger.KotlinCoroutinesAsyncStackTraceProvider import org.jetbrains.kotlin.idea.debugger.KotlinCoroutinesAsyncStackTraceProvider
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
import org.jetbrains.kotlin.idea.debugger.evaluate.createExecutionContext
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import javax.swing.event.TreeModelEvent import javax.swing.event.TreeModelEvent
@@ -52,7 +51,7 @@ import javax.swing.event.TreeModelListener
*/ */
class CoroutinesDebuggerTree(project: Project) : DebuggerTree(project) { class CoroutinesDebuggerTree(project: Project) : DebuggerTree(project) {
private val logger = Logger.getInstance(this::class.java) private val logger = Logger.getInstance(this::class.java)
private var lastSuspendContextDump: Pair<WeakReference<SuspendContextImpl>, Either<Throwable, List<CoroutineState>>>? = null private var lastSuspendContextCache: Cache? = null
override fun createNodeManager(project: Project): NodeManagerImpl { override fun createNodeManager(project: Project): NodeManagerImpl {
return object : NodeManagerImpl(project, this) { return object : NodeManagerImpl(project, this) {
@@ -227,11 +226,13 @@ class CoroutinesDebuggerTree(project: Project) : DebuggerTree(project) {
pos: XSourcePosition pos: XSourcePosition
): Pair<XExecutionStack, SyntheticStackFrame>? { ): Pair<XExecutionStack, SyntheticStackFrame>? {
val context = DebuggerManagerEx.getInstanceEx(project).context val context = DebuggerManagerEx.getInstanceEx(project).context
val proxy = context.suspendContext?.thread ?: return null val suspendContext = context.suspendContext ?: return null
val executionStack = val proxy = suspendContext.thread ?: return null
JavaExecutionStack(proxy, context.debugProcess!!, false) val executionStack = JavaExecutionStack(proxy, suspendContext.debugProcess, false)
executionStack.initTopFrame() executionStack.initTopFrame()
val execContext = context.createExecutionContext() ?: return null val evalContext = context.createEvaluationContext()
val frameProxy = evalContext?.frameProxy ?: return null
val execContext = ExecutionContext(evalContext, frameProxy)
val continuation = descriptor.continuation // guaranteed that it is a BaseContinuationImpl val continuation = descriptor.continuation // guaranteed that it is a BaseContinuationImpl
val aMethod = (continuation.type() as ClassType).concreteMethodByName( val aMethod = (continuation.type() as ClassType).concreteMethodByName(
"getStackTraceElement", "getStackTraceElement",
@@ -255,6 +256,7 @@ class CoroutinesDebuggerTree(project: Project) : DebuggerTree(project) {
descriptor: NodeDescriptorImpl, descriptor: NodeDescriptorImpl,
evalContext: EvaluationContextImpl evalContext: EvaluationContextImpl
) { ) {
val creationStackTraceSeparator = "\b\b\b" // the "\b\b\b" is used for creation stacktrace separator in kotlinx.coroutines
if (descriptor !is CoroutineDescriptorImpl) { if (descriptor !is CoroutineDescriptorImpl) {
if (descriptor is CreationFramesDescriptor) { if (descriptor is CreationFramesDescriptor) {
val threadProxy = debuggerContext.suspendContext?.thread ?: return val threadProxy = debuggerContext.suspendContext?.thread ?: return
@@ -298,7 +300,7 @@ class CoroutinesDebuggerTree(project: Project) : DebuggerTree(project) {
val proxy = threadProxy.forceFrames().first() val proxy = threadProxy.forceFrames().first()
// the thread is paused on breakpoint - it has at least one frame // the thread is paused on breakpoint - it has at least one frame
for (it in descriptor.state.stackTrace) { for (it in descriptor.state.stackTrace) {
if (it.className.startsWith("\b\b\b")) break if (it.className.startsWith(creationStackTraceSeparator)) break
children.add(createCoroutineFrameDescriptor(descriptor, evalContext, it, proxy)) children.add(createCoroutineFrameDescriptor(descriptor, evalContext, it, proxy))
} }
} }
@@ -306,7 +308,7 @@ class CoroutinesDebuggerTree(project: Project) : DebuggerTree(project) {
} }
} }
val trace = descriptor.state.stackTrace val trace = descriptor.state.stackTrace
val index = trace.indexOfFirst { it.className.startsWith("\b\b\b") } val index = trace.indexOfFirst { it.className.startsWith(creationStackTraceSeparator) }
children.add(myNodeManager.createNode(CreationFramesDescriptor(trace.subList(index + 1, trace.size)), evalContext)) children.add(myNodeManager.createNode(CreationFramesDescriptor(trace.subList(index + 1, trace.size)), evalContext))
} }
@@ -371,7 +373,7 @@ class CoroutinesDebuggerTree(project: Project) : DebuggerTree(project) {
override fun isExpandable(node: DebuggerTreeNodeImpl): Boolean { override fun isExpandable(node: DebuggerTreeNodeImpl): Boolean {
val descriptor = node.descriptor val descriptor = node.descriptor
return if (descriptor is StackFrameDescriptor) return false else descriptor.isExpandable return if (descriptor is StackFrameDescriptor) false else descriptor.isExpandable
} }
override fun build(context: DebuggerContextImpl) { override fun build(context: DebuggerContextImpl) {
@@ -402,11 +404,11 @@ class CoroutinesDebuggerTree(project: Project) : DebuggerTree(project) {
} }
val evaluationContext = EvaluationContextImpl(suspendContext, suspendContext.frameProxy) val evaluationContext = EvaluationContextImpl(suspendContext, suspendContext.frameProxy)
val executionContext = ExecutionContext(evaluationContext, suspendContext.frameProxy ?: return) val executionContext = ExecutionContext(evaluationContext, suspendContext.frameProxy ?: return)
val cache = lastSuspendContextDump val cache = lastSuspendContextCache
val states = if (cache != null && cache.first.get() === suspendContext) { val states = if (cache != null && cache.first.get() === suspendContext) {
cache.second cache.second
} else CoroutinesDebugProbesProxy.dumpCoroutines(executionContext).apply { } else CoroutinesDebugProbesProxy.dumpCoroutines(executionContext).apply {
lastSuspendContextDump = WeakReference(suspendContext) to this lastSuspendContextCache = WeakReference(suspendContext) to this
} }
// if suspend context hasn't changed - use last dump, else compute new // if suspend context hasn't changed - use last dump, else compute new
if (states.isLeft) { if (states.isLeft) {
@@ -442,3 +444,5 @@ class CoroutinesDebuggerTree(project: Project) : DebuggerTree(project) {
} }
} }
private typealias Cache = Pair<WeakReference<SuspendContextImpl>, Either<Throwable, List<CoroutineState>>>
@@ -120,6 +120,7 @@ class CoroutinesPanel(project: Project, stateManager: DebuggerStateManager) : De
super.dispose() super.dispose()
} }
// copied from com.intellij.debugger.ui.impl.ThreadsPanel.updateNodeLabels
private fun updateNodeLabels(from: DebuggerTreeNodeImpl) { private fun updateNodeLabels(from: DebuggerTreeNodeImpl) {
val children = from.children() val children = from.children()
try { try {
@@ -25,6 +25,7 @@ import com.intellij.openapi.util.IconLoader
import com.sun.jdi.ClassType import com.sun.jdi.ClassType
import com.sun.jdi.ObjectReference import com.sun.jdi.ObjectReference
import org.jetbrains.kotlin.idea.IconExtensionChooser import org.jetbrains.kotlin.idea.IconExtensionChooser
import javaslang.control.Either
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
import javax.swing.Icon import javax.swing.Icon
@@ -37,29 +38,16 @@ class CoroutineData(private val state: CoroutineState) : DescriptorData<Coroutin
return CoroutineDescriptorImpl(state) return CoroutineDescriptorImpl(state)
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?) = if (other !is CoroutineData) false else state.name == other.state.name
return if (other !is CoroutineData) {
false
} else state.name == other.state.name
}
override fun hashCode(): Int { override fun hashCode() = state.name.hashCode()
return state.name.hashCode()
}
override fun getDisplayKey(): DisplayKey<CoroutineDescriptorImpl> { override fun getDisplayKey(): DisplayKey<CoroutineDescriptorImpl> = SimpleDisplayKey(state.name)
return SimpleDisplayKey(state.name)
}
} }
class CoroutineDescriptorImpl(val state: CoroutineState) : NodeDescriptorImpl() { class CoroutineDescriptorImpl(val state: CoroutineState) : NodeDescriptorImpl() {
var suspendContext: SuspendContextImpl? = null var suspendContext: SuspendContextImpl? = null
val icon: Icon lateinit var icon: Icon
get() = when {
state.isSuspended -> AllIcons.Debugger.ThreadSuspended
state.state == CoroutineState.State.CREATED -> AllIcons.Debugger.ThreadStates.Idle
else -> AllIcons.Debugger.ThreadRunning
}
override fun getName(): String? { override fun getName(): String? {
return state.name return state.name
@@ -76,30 +64,35 @@ class CoroutineDescriptorImpl(val state: CoroutineState) : NodeDescriptorImpl()
return state.state != CoroutineState.State.CREATED return state.state != CoroutineState.State.CREATED
} }
override fun setContext(context: EvaluationContextImpl?) { private fun calcIcon() = when {
state.isSuspended -> AllIcons.Debugger.ThreadSuspended
state.state == CoroutineState.State.CREATED -> AllIcons.Debugger.ThreadStates.Idle
else -> AllIcons.Debugger.ThreadRunning
}
override fun setContext(context: EvaluationContextImpl?) {
icon = calcIcon()
} }
} }
class CoroutineStackFrameData private constructor(val state: CoroutineState, private val proxy: StackFrameProxyImpl) : class CoroutineStackFrameData private constructor(val state: CoroutineState, private val proxy: StackFrameProxyImpl) :
DescriptorData<NodeDescriptorImpl>() { DescriptorData<NodeDescriptorImpl>() {
private var frame: StackTraceElement? = null private lateinit var frame: Either<StackTraceElement, StackFrameItem>
private var frameItem: StackFrameItem? = null
constructor(state: CoroutineState, frame: StackTraceElement, proxy: StackFrameProxyImpl) : this(state, proxy) { constructor(state: CoroutineState, frame: StackTraceElement, proxy: StackFrameProxyImpl) : this(state, proxy) {
this.frame = frame this.frame = Either.left(frame)
} }
constructor(state: CoroutineState, frameItem: StackFrameItem, proxy: StackFrameProxyImpl) : this(state, proxy) { constructor(state: CoroutineState, frameItem: StackFrameItem, proxy: StackFrameProxyImpl) : this(state, proxy) {
this.frameItem = frameItem this.frame = Either.right(frameItem)
} }
override fun hashCode() = frame?.hashCode() ?: frameItem.hashCode() override fun hashCode(): Int {
return if (frame.isLeft) frame.left.hashCode() else frame.get().hashCode()
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
return if (other is CoroutineStackFrameData) { return other is CoroutineStackFrameData && frame == other.frame
other.frame == frame && other.frameItem == frameItem
} else false
} }
/** /**
@@ -107,12 +100,10 @@ class CoroutineStackFrameData private constructor(val state: CoroutineState, pri
* or [AsyncStackFrameDescriptor] according to current frame * or [AsyncStackFrameDescriptor] according to current frame
*/ */
override fun createDescriptorImpl(project: Project): NodeDescriptorImpl { override fun createDescriptorImpl(project: Project): NodeDescriptorImpl {
val frame = frame ?: return AsyncStackFrameDescriptor( val isLeft = frame.isLeft
state, if (!isLeft) return AsyncStackFrameDescriptor(state, frame.get(), proxy)
frameItem!!,
proxy
)
// check whether last fun is suspend fun // check whether last fun is suspend fun
val frame = frame.left
val suspendContext = val suspendContext =
DebuggerManagerEx.getInstanceEx(project).context.suspendContext ?: return EmptyStackFrameDescriptor( DebuggerManagerEx.getInstanceEx(project).context.suspendContext ?: return EmptyStackFrameDescriptor(
frame, frame,
@@ -212,9 +203,7 @@ class EmptyStackFrameDescriptor(val frame: StackTraceElement, proxy: StackFrameP
} }
class CreationFramesDescriptor(val frames: List<StackTraceElement>) : NodeDescriptorImpl() { class CreationFramesDescriptor(val frames: List<StackTraceElement>) : NodeDescriptorImpl() {
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String { override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?) = name
return "Coroutine creation stack trace"
}
override fun setContext(context: EvaluationContextImpl?) {} override fun setContext(context: EvaluationContextImpl?) {}
override fun getName() = "Coroutine creation stack trace" override fun getName() = "Coroutine creation stack trace"
@@ -101,8 +101,3 @@ class ExecutionContext(val evaluationContext: EvaluationContextImpl, val framePr
DebuggerUtilsEx.keep(reference, evaluationContext) DebuggerUtilsEx.keep(reference, evaluationContext)
} }
} }
fun DebuggerContextImpl.createExecutionContext(): ExecutionContext? {
val context = createEvaluationContext()
return ExecutionContext(context ?: return null, frameProxy ?: return null)
}