From 5975251a3268dd0ebb58f153be9fe8df4becb611 Mon Sep 17 00:00:00 2001 From: Aleksandr Prokopyev Date: Mon, 8 Jul 2019 21:06:34 +0300 Subject: [PATCH] Debugger: Coroutines stack frames with variables & coroutine dumps --- .../kotlin/generators/tests/GenerateTests.kt | 2 + ...KotlinCoroutinesAsyncStackTraceProvider.kt | 4 +- .../coroutines/CoroutineDumpAction.kt | 106 +++++ .../debugger/coroutines/CoroutineDumpPanel.kt | 300 ++++++++++++ .../debugger/coroutines/CoroutineState.kt | 87 ++++ .../CoroutinesDebugConfigurationExtension.kt | 126 +++++ .../coroutines/CoroutinesDebugProbesProxy.kt | 185 ++++++++ .../coroutines/CoroutinesDebuggerTree.kt | 444 ++++++++++++++++++ .../debugger/coroutines/CoroutinesPanel.kt | 161 +++++++ .../coroutines/SyntheticStackFrame.kt | 46 ++ .../idea/debugger/coroutines/descriptors.kt | 222 +++++++++ .../debugger/evaluate/ExecutionContext.kt | 7 + idea/resources/META-INF/jvm.xml | 23 + .../tinyApp/src/coroutines/noCoroutines.kt | 6 + .../tinyApp/src/coroutines/noCoroutines.out | 7 + .../tinyApp/src/coroutines/threeCoroutines.kt | 13 + .../src/coroutines/threeCoroutines.out | 10 + .../tinyApp/src/coroutines/twoDumps.kt | 31 ++ .../tinyApp/src/coroutines/twoDumps.out | 12 + .../coroutines/AbstractCoroutineDumpTest.kt | 87 ++++ .../CoroutineDumpTestGenerated.java | 46 ++ 21 files changed, 1923 insertions(+), 2 deletions(-) create mode 100644 idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpAction.kt create mode 100644 idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpPanel.kt create mode 100644 idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineState.kt create mode 100644 idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugConfigurationExtension.kt create mode 100644 idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugProbesProxy.kt create mode 100644 idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebuggerTree.kt create mode 100644 idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesPanel.kt create mode 100644 idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/SyntheticStackFrame.kt create mode 100644 idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/descriptors.kt create mode 100644 idea/testData/debugger/tinyApp/src/coroutines/noCoroutines.kt create mode 100644 idea/testData/debugger/tinyApp/src/coroutines/noCoroutines.out create mode 100644 idea/testData/debugger/tinyApp/src/coroutines/threeCoroutines.kt create mode 100644 idea/testData/debugger/tinyApp/src/coroutines/threeCoroutines.out create mode 100644 idea/testData/debugger/tinyApp/src/coroutines/twoDumps.kt create mode 100644 idea/testData/debugger/tinyApp/src/coroutines/twoDumps.out create mode 100644 idea/tests/org/jetbrains/kotlin/idea/debugger/coroutines/AbstractCoroutineDumpTest.kt create mode 100644 idea/tests/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpTestGenerated.java diff --git a/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt b/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt index f26240bb6ec..236daf94c98 100644 --- a/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt +++ b/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt @@ -55,6 +55,8 @@ import org.jetbrains.kotlin.idea.conversion.copy.AbstractLiteralKotlinToKotlinCo import org.jetbrains.kotlin.idea.conversion.copy.AbstractLiteralTextToKotlinCopyPasteTest import org.jetbrains.kotlin.idea.conversion.copy.AbstractTextJavaToKotlinCopyPasteConversionTest import org.jetbrains.kotlin.idea.coverage.AbstractKotlinCoverageOutputFilesTest +import org.jetbrains.kotlin.idea.debugger.* +import org.jetbrains.kotlin.idea.debugger.coroutines.AbstractCoroutineDumpTest import org.jetbrains.kotlin.idea.debugger.evaluate.* import org.jetbrains.kotlin.idea.debugger.test.sequence.exec.AbstractSequenceTraceTestCase import org.jetbrains.kotlin.idea.debugger.test.* diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinCoroutinesAsyncStackTraceProvider.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinCoroutinesAsyncStackTraceProvider.kt index 7b30985fd01..f42a2c76ee8 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinCoroutinesAsyncStackTraceProvider.kt +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinCoroutinesAsyncStackTraceProvider.kt @@ -140,7 +140,7 @@ class KotlinCoroutinesAsyncStackTraceProvider : KotlinCoroutinesAsyncStackTraceP return GeneratedLocation(context.debugProcess, locationClass, methodName, lineNumber) } - private fun AsyncStackTraceContext.getSpilledVariables(continuation: ObjectReference): List? { + fun AsyncStackTraceContext.getSpilledVariables(continuation: ObjectReference): List? { val getSpilledVariableFieldMappingMethod = debugMetadataKtType.methodsByName( "getSpilledVariableFieldMapping", "(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)[Ljava/lang/String;" @@ -188,7 +188,7 @@ class KotlinCoroutinesAsyncStackTraceProvider : KotlinCoroutinesAsyncStackTraceP } } - private class AsyncStackTraceContext( + class AsyncStackTraceContext( val context: ExecutionContext, val method: Method, val debugMetadataKtType: ClassType diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpAction.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpAction.kt new file mode 100644 index 00000000000..daade0886b0 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpAction.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines + +import com.intellij.debugger.DebuggerManagerEx +import com.intellij.debugger.engine.events.SuspendContextCommandImpl +import com.intellij.debugger.impl.DebuggerUtilsEx +import com.intellij.execution.filters.ExceptionFilters +import com.intellij.execution.filters.TextConsoleBuilderFactory +import com.intellij.execution.ui.RunnerLayoutUi +import com.intellij.execution.ui.layout.impl.RunnerContentUi +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.MessageType +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.registry.Registry +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.util.text.DateFormatUtil +import com.intellij.xdebugger.impl.XDebuggerManagerImpl +import org.jetbrains.kotlin.idea.debugger.evaluate.createExecutionContext + +@Suppress("ComponentNotRegistered") +class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate { + private val logger = Logger.getInstance(this::class.java) + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val context = DebuggerManagerEx.getInstanceEx(project).context + val session = context.debuggerSession + if (session != null && session.isAttached) { + val process = context.debugProcess ?: return + process.managerThread.schedule(object : SuspendContextCommandImpl(context.suspendContext) { + override fun contextAction() { + val execContext = context.createExecutionContext() ?: return + val states = CoroutinesDebugProbesProxy.dumpCoroutines(execContext) + if (states.isLeft) { + logger.warn(states.left) + XDebuggerManagerImpl.NOTIFICATION_GROUP + .createNotification( + "Coroutine dump failed. See log", + MessageType.ERROR + ).notify(project) + return + } + val f = fun() { + addCoroutineDump( + project, + states.get(), + session.xDebugSession?.ui ?: return, + session.searchScope + ) + } + ApplicationManager.getApplication().invokeLater(f, ModalityState.NON_MODAL) + } + }) + } + } + + /** + * Analog of [DebuggerUtilsEx.addThreadDump]. + */ + fun addCoroutineDump(project: Project, coroutines: List, ui: RunnerLayoutUi, searchScope: GlobalSearchScope) { + val consoleBuilder = TextConsoleBuilderFactory.getInstance().createBuilder(project) + consoleBuilder.filters(ExceptionFilters.getFilters(searchScope)) + val consoleView = consoleBuilder.console + val toolbarActions = DefaultActionGroup() + consoleView.allowHeavyFilters() + val panel = CoroutineDumpPanel(project, consoleView, toolbarActions, coroutines) + + val id = "DumpKt " + DateFormatUtil.formatTimeWithSeconds(System.currentTimeMillis()) + val content = ui.createContent(id, panel, id, null, null).apply { + putUserData(RunnerContentUi.LIGHTWEIGHT_CONTENT_MARKER, true) + isCloseable = true + description = "Coroutine Dump" + } + ui.addContent(content) + ui.selectAndFocus(content, true, true) + Disposer.register(content, consoleView) + } + + override fun update(e: AnActionEvent) { + val presentation = e.presentation + val project = e.project + if (project == null) { + presentation.isEnabled = false + presentation.isVisible = false + return + } + // cannot be called when no SuspendContext + if (DebuggerManagerEx.getInstanceEx(project).context.suspendContext == null) { + presentation.isEnabled = false + return + } + val debuggerSession = DebuggerManagerEx.getInstanceEx(project).context.debuggerSession + presentation.isEnabled = debuggerSession != null && debuggerSession.isAttached && Registry.`is`("kotlin.debugger" + ".coroutines") + presentation.isVisible = presentation.isEnabled + } +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpPanel.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpPanel.kt new file mode 100644 index 00000000000..345467d99c5 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpPanel.kt @@ -0,0 +1,300 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines + +import com.intellij.codeInsight.highlighting.HighlightManager +import com.intellij.execution.ui.ConsoleView +import com.intellij.icons.AllIcons +import com.intellij.icons.AllIcons.Debugger.ThreadStates.Daemon_sign +import com.intellij.ide.DataManager +import com.intellij.ide.ExporterToTextFile +import com.intellij.ide.ui.UISettings +import com.intellij.notification.NotificationGroup +import com.intellij.openapi.actionSystem.* +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.colors.EditorColors +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.event.DocumentListener +import com.intellij.openapi.ide.CopyPasteManager +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.MessageType +import com.intellij.openapi.ui.Splitter +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.wm.IdeFocusManager +import com.intellij.openapi.wm.ToolWindowId +import com.intellij.ui.* +import com.intellij.ui.components.JBList +import com.intellij.unscramble.AnalyzeStacktraceUtil +import com.intellij.util.PlatformIcons +import com.intellij.util.ui.EmptyIcon +import java.awt.BorderLayout +import java.awt.Color +import java.awt.datatransfer.StringSelection +import java.io.File +import javax.swing.* +import javax.swing.event.DocumentEvent + +/** + * Panel with dump of coroutines + */ +class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActions: DefaultActionGroup, val dump: List) : + JPanel(BorderLayout()), DataProvider { + private var exporterToTextFile: ExporterToTextFile + private var mergedDump = ArrayList() + val filterField = SearchTextField() + val filterPanel = JPanel(BorderLayout()) + private val coroutinesList = JBList(DefaultListModel()) + + init { + mergedDump.addAll(dump) + + filterField.addDocumentListener(object : DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + updateCoroutinesList() + } + }) + + filterPanel.apply { + add(JLabel("Filter:"), BorderLayout.WEST) + add(filterField) + isVisible = false + } + + coroutinesList.apply { + cellRenderer = CoroutineListCellRenderer() + selectionMode = ListSelectionModel.SINGLE_SELECTION + addListSelectionListener { + val index = selectedIndex + if (index >= 0) { + val selection = model.getElementAt(index) as CoroutineState + AnalyzeStacktraceUtil.printStacktrace(consoleView, selection.stringStackTrace) + } else { + AnalyzeStacktraceUtil.printStacktrace(consoleView, "") + } + repaint() + } + } + + exporterToTextFile = createToFileExporter(project, dump) + + val filterAction = FilterAction().apply { + registerCustomShortcutSet( + ActionManager.getInstance().getAction(IdeActions.ACTION_FIND).shortcutSet, + coroutinesList + ) + } + toolbarActions.apply { + add(filterAction) + add(CopyToClipboardAction(dump, project)) + add(ActionManager.getInstance().getAction(IdeActions.ACTION_EXPORT_TO_TEXT_FILE)) + add(MergeStacktracesAction()) + } + add( + ActionManager.getInstance() + .createActionToolbar("CoroutinesDump", toolbarActions, false).component, + BorderLayout.WEST + ) + + val leftPanel = JPanel(BorderLayout()).apply { + add(filterPanel, BorderLayout.NORTH) + add(ScrollPaneFactory.createScrollPane(coroutinesList, SideBorder.LEFT or SideBorder.RIGHT), BorderLayout.CENTER) + } + + val splitter = Splitter(false, 0.3f).apply { + firstComponent = leftPanel + secondComponent = consoleView.component + } + add(splitter, BorderLayout.CENTER) + + ListSpeedSearch(coroutinesList).comparator = SpeedSearchComparator(false, true) + + updateCoroutinesList() + + val editor = CommonDataKeys.EDITOR.getData( + DataManager.getInstance() + .getDataContext(consoleView.preferredFocusableComponent) + ) + editor?.document?.addDocumentListener(object : DocumentListener { + override fun documentChanged(e: com.intellij.openapi.editor.event.DocumentEvent) { + val filter = filterField.text + if (StringUtil.isNotEmpty(filter)) { + highlightOccurrences(filter, project, editor) + } + } + }, consoleView) + } + + private fun updateCoroutinesList() { + val text = if (filterPanel.isVisible) filterField.text else "" + val selection = coroutinesList.selectedValue + val model = coroutinesList.model as DefaultListModel + model.clear() + var selectedIndex = 0 + 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)) { + + model.addElement(state) + if (selection === state) { + selectedIndex = index + } + index++ + } + } + if (!model.isEmpty) { + coroutinesList.selectedIndex = selectedIndex + } + coroutinesList.revalidate() + coroutinesList.repaint() + } + + internal fun highlightOccurrences(filter: String, project: Project, editor: Editor) { + val highlightManager = HighlightManager.getInstance(project) + val colorManager = EditorColorsManager.getInstance() + val attributes = colorManager.globalScheme.getAttributes(EditorColors.TEXT_SEARCH_RESULT_ATTRIBUTES) + val documentText = editor.document.text + var i = -1 + while (true) { + val nextOccurrence = StringUtil.indexOfIgnoreCase(documentText, filter, i + 1) + if (nextOccurrence < 0) { + break + } + i = nextOccurrence + highlightManager.addOccurrenceHighlight( + editor, i, i + filter.length, attributes, + HighlightManager.HIDE_BY_TEXT_CHANGE, null, null + ) + } + } + + override fun getData(dataId: String): Any? = if (PlatformDataKeys.EXPORTER_TO_TEXT_FILE.`is`(dataId)) exporterToTextFile else null + + private fun getCoroutineStateIcon(state: CoroutineState): Icon { + return when (state.state) { + CoroutineState.State.RUNNING -> LayeredIcon(AllIcons.Actions.Resume, Daemon_sign) + CoroutineState.State.SUSPENDED -> AllIcons.Actions.Pause + else -> EmptyIcon.create(6) + } + } + + private fun getAttributes(state: CoroutineState): SimpleTextAttributes { + return when { + state.isSuspended -> SimpleTextAttributes.GRAY_ATTRIBUTES + state.isEmptyStackTrace -> SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, Color.GRAY.brighter()) + else -> SimpleTextAttributes.REGULAR_ATTRIBUTES + + } + } + + private inner class CoroutineListCellRenderer : ColoredListCellRenderer() { + + override fun customizeCellRenderer(list: JList<*>, value: Any, index: Int, selected: Boolean, hasFocus: Boolean) { + val state = value as CoroutineState + icon = getCoroutineStateIcon(state) + val attrs = getAttributes(state) + append(state.name + " (", attrs) + var detail: String? = state.state.name + if (detail == null) { + detail = state.state.name + } + if (detail.length > 30) { + detail = detail.substring(0, 30) + "..." + } + append(detail, attrs) + append(")", attrs) + } + } + + private inner class FilterAction : + ToggleAction("Filter", "Show only threads containing a specific string", AllIcons.General.Filter), + DumbAware { + + override fun isSelected(e: AnActionEvent): Boolean { + return filterPanel.isVisible + } + + override fun setSelected(e: AnActionEvent, state: Boolean) { + filterPanel.isVisible = state + if (state) { + IdeFocusManager.getInstance(AnAction.getEventProject(e)).requestFocus(filterField, true) + filterField.selectText() + } + updateCoroutinesList() + } + } + + @Suppress("DEPRECATION") + private inner class MergeStacktracesAction : + ToggleAction( + "Merge Identical Stacktraces", + "Group coroutines with identical stacktraces", + AllIcons.General.CollapseAll + ), + DumbAware { + + override fun isSelected(e: AnActionEvent): Boolean { + return UISettings.instance.state.mergeEqualStackTraces + } + + override fun setSelected(e: AnActionEvent, state: Boolean) { + UISettings.instance.state.mergeEqualStackTraces = state + updateCoroutinesList() + } + } + + private class CopyToClipboardAction(private val myCoroutinesDump: List, private val myProject: Project) : + DumbAwareAction("Copy to Clipboard", "Copy whole coroutine dump to clipboard", PlatformIcons.COPY_ICON) { + + override fun actionPerformed(e: AnActionEvent) { + val buf = StringBuilder() + buf.append("Full coroutine dump").append("\n\n") + for (state in myCoroutinesDump) { + buf.append(state.stringStackTrace).append("\n\n") + } + CopyPasteManager.getInstance().setContents(StringSelection(buf.toString())) + + group.createNotification( + "Full coroutine dump was successfully copied to clipboard", + MessageType.INFO + ).notify(myProject) + } + + private val group = NotificationGroup.toolWindowGroup("Analyze coroutine dump", ToolWindowId.RUN, false) + } + + private fun createToFileExporter(project: Project, states: List): ExporterToTextFile { + return MyToFileExporter(project, states) + } + + private class MyToFileExporter(private val myProject: Project, private val states: List) : + ExporterToTextFile { + + 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(): String { + val baseDir = myProject.baseDir + return if (baseDir != null) { + baseDir.presentableUrl + File.separator + defaultReportFileName + } else "" + } + + override fun canExport(): Boolean { + return states.isNotEmpty() + } + + private val defaultReportFileName = "coroutines_report.txt" + } +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineState.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineState.kt new file mode 100644 index 00000000000..b7a7b2e3c6d --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineState.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines + +import com.sun.jdi.* +import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext +import org.jetbrains.kotlin.idea.debugger.isSubtype + +/** + * Represents state of a coroutine. + * @see `kotlinx.coroutines.debug.CoroutineInfo` + */ +class CoroutineState( + val name: String, + val state: State, + val thread: ThreadReference? = null, + val stackTrace: List, + val frame: ObjectReference? +) { + val isSuspended: Boolean = state == State.SUSPENDED + val isEmptyStackTrace: Boolean by lazy { stackTrace.isEmpty() } + val stringStackTrace: String by lazy { + buildString { + appendln("\"$name\", state: $state") + stackTrace.forEach { + appendln("\t$it") + } + } + } + + /** + * Finds previous Continuation for this Continuation (completion field in BaseContinuationImpl) + * @return null if given ObjectReference is not a BaseContinuationImpl instance or completion is null + */ + private fun getNextFrame(continuation: ObjectReference, context: ExecutionContext): ObjectReference? { + val type = continuation.type() as ClassType + if (!type.isSubtype("kotlin.coroutines.jvm.internal.BaseContinuationImpl")) return null + val next = type.concreteMethodByName("getCompletion", "()Lkotlin/coroutines/Continuation;") + return context.invokeMethod(continuation, next, emptyList()) as? ObjectReference + } + + /** + * Find continuation for the [stackTraceElement] + * Gets current CoroutineInfo.lastObservedFrame and finds next frames in it until null or needed stackTraceElement is found + * @return null if matching continuation is not found or is not BaseContinuationImpl + */ + fun getContinuation(stackTraceElement: StackTraceElement, context: ExecutionContext): ObjectReference? { + var continuation = frame ?: return null + 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 "" + } + val lineNumber = { + val trace = context.invokeMethod(continuation, getTrace, emptyList()) as? ObjectReference + if (trace != null) + (context.invokeMethod(trace, getLineNumber, emptyList()) as IntegerValue).value() + else -239 // invalid line number (but well-educated) + } + + while (continuation.type().isSubtype(baseType) + && (stackTraceElement.className != className() || stackTraceElement.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 + } + + enum class State { + RUNNING, + SUSPENDED, + CREATED + } +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugConfigurationExtension.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugConfigurationExtension.kt new file mode 100644 index 00000000000..b1e7bf50691 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugConfigurationExtension.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ +package org.jetbrains.kotlin.idea.debugger.coroutines + +import com.intellij.debugger.DebuggerInvocationUtil +import com.intellij.debugger.DebuggerManagerEx +import com.intellij.debugger.actions.ThreadDumpAction +import com.intellij.debugger.impl.DebuggerSession +import com.intellij.execution.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 +import com.intellij.execution.ui.RunnerLayoutUi +import com.intellij.execution.ui.layout.PlaceInGrid +import com.intellij.execution.ui.layout.impl.RunnerContentUi +import com.intellij.execution.ui.layout.impl.RunnerLayoutUiImpl +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionPlaces +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.registry.Registry +import com.intellij.ui.content.ContentManagerAdapter +import com.intellij.ui.content.ContentManagerEvent +import com.intellij.util.messages.MessageBusConnection +import com.intellij.xdebugger.XDebugProcess +import com.intellij.xdebugger.XDebuggerManager +import com.intellij.xdebugger.XDebuggerManagerListener +import org.jetbrains.kotlin.psi.UserDataProperty + +/** + * Installs coroutines debug agent and coroutines tab if `kotlinx.coroutines.debug` dependency is found + */ +@Suppress("IncompatibleAPI") +class CoroutinesDebugConfigurationExtension : RunConfigurationExtension() { + private var Project.listenerCreated: Boolean? by UserDataProperty(Key.create("COROUTINES_DEBUG_TAB_CREATE_LISTENER")) + + override fun isApplicableFor(configuration: RunConfigurationBase<*>): Boolean { + return Registry.`is`("kotlin.debugger" + ".coroutines") + } + + override fun ?> updateJavaParameters( + configuration: T, + params: JavaParameters?, + runnerSettings: RunnerSettings? + ) { + if (!Registry.`is`("kotlin.debugger" + ".coroutines")) return + if (runnerSettings is DebuggingRunnerData && params != null + && params.classPath != null + && params.classPath.pathList.isNotEmpty() + ) { + params.classPath.pathList.forEach { + if (!it.contains("kotlinx-coroutines-debug")) return@forEach + // if debug library is included into project, add agent which installs probes + params.vmParametersList?.add("-javaagent:$it") + params.vmParametersList?.add("-ea") + val project = (configuration as RunConfigurationBase<*>).project + // add listener to put coroutines tab into debugger tab + if (project.listenerCreated != true) { // prevent multiple listeners creation + val connection = project.messageBus.connect() + connection.subscribe( + XDebuggerManager.TOPIC, createListener(project, connection) + ) + project.listenerCreated = true + return + } + } + + } + } + + private fun createListener(project: Project, connection: MessageBusConnection): XDebuggerManagerListener { + return object : XDebuggerManagerListener { + override fun processStarted(debugProcess: XDebugProcess) { + DebuggerInvocationUtil.swingInvokeLater(project) { + val session = DebuggerManagerEx.getInstanceEx(project).context.debuggerSession + if (session != null) + registerCoroutinesPanel(session.xDebugSession?.ui ?: return@swingInvokeLater, session) + } + } + + override fun processStopped(debugProcess: XDebugProcess) { + connection.disconnect() + project.listenerCreated = false + } + } + } + + /** + * Adds panel to XDebugSessionTab + */ + private fun registerCoroutinesPanel(ui: RunnerLayoutUi, session: DebuggerSession) { + val panel = CoroutinesPanel(session.project, session.contextManager) + val content = ui.createContent( + "CoroutinesContent", panel, "Coroutines", // TODO(design) + AllIcons.Debugger.ThreadGroup, null + ) + content.isCloseable = false + ui.addContent(content, 0, PlaceInGrid.left, true) + ui.addListener(object : ContentManagerAdapter() { + override fun selectionChanged(event: ContentManagerEvent) { + if (event.content === content) { + if (content.isSelected) { + panel.setUpdateEnabled(true) + if (panel.isRefreshNeeded) { + panel.rebuildIfVisible(DebuggerSession.Event.CONTEXT) + } + } else { + panel.setUpdateEnabled(false) + } + } + } + }, content) + // add coroutine dump button: due to api problem left toolbar is copied, modified and reset to tab + val runnerContent = (ui.options as RunnerLayoutUiImpl).getData(RunnerContentUi.KEY.name) as RunnerContentUi + val modifiedActions = runnerContent.getActions(true) + val pos = modifiedActions.indexOfLast { it is ThreadDumpAction } + modifiedActions.add(pos + 1, ActionManager.getInstance().getAction("Kotlin.XDebugger.CoroutinesDump")) + ui.options.setLeftToolbar(DefaultActionGroup(modifiedActions), ActionPlaces.DEBUGGER_TOOLBAR) + } + +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugProbesProxy.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugProbesProxy.kt new file mode 100644 index 00000000000..dc7f05123b7 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugProbesProxy.kt @@ -0,0 +1,185 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ +package org.jetbrains.kotlin.idea.debugger.coroutines + +import com.intellij.debugger.engine.DebugProcess +import com.intellij.openapi.util.Key +import com.sun.jdi.* +import com.sun.tools.jdi.StringReferenceImpl +import javaslang.control.Either +import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext +import org.jetbrains.kotlin.psi.UserDataProperty + +object CoroutinesDebugProbesProxy { + private const val DEBUG_PACKAGE = "kotlinx.coroutines.debug" + private var DebugProcess.references by UserDataProperty(Key.create("COROUTINES_DEBUG_REFERENCES")) + + @Suppress("unused") + fun install(context: ExecutionContext) { + val debugProbes = context.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType + val instance = with(debugProbes) { getValue(fieldByName("INSTANCE")) as ObjectReference } + val install = debugProbes.concreteMethodByName("install", "()V") + context.invokeMethod(instance, install, emptyList()) + } + + @Suppress("unused") + fun uninstall(context: ExecutionContext) { + val debugProbes = context.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType + val instance = with(debugProbes) { getValue(fieldByName("INSTANCE")) as ObjectReference } + val uninstall = debugProbes.concreteMethodByName("uninstall", "()V") + context.invokeMethod(instance, uninstall, emptyList()) + } + + /** + * Invokes DebugProbes from debugged process's classpath and returns states of coroutines + * Should be invoked on debugger manager thread + */ + fun dumpCoroutines(context: ExecutionContext): Either> { + try { + if (context.debugProcess.references == null) { + context.debugProcess.references = ProcessReferences(context) + } + val refs = context.debugProcess.references!! // already initialized if it was null + + // get dump + val infoList = context.invokeMethod(refs.instance, refs.dumpMethod, emptyList()) as ObjectReference + context.keepReference(infoList) + val size = (context.invokeMethod(infoList, refs.getSize, emptyList()) as IntegerValue).value() + + return Either.right(List(size) { + val index = context.vm.mirrorOf(it) + val elem = context.invokeMethod(infoList, refs.getElement, listOf(index)) as ObjectReference + val name = getName(context, elem, refs) + val state = getState(context, elem, refs) + val thread = getLastObservedThread(elem, refs.threadRef) + CoroutineState( + name, + CoroutineState.State.valueOf(state), + thread, + getStackTrace(elem, refs, context), + elem.getValue(refs.continuation) as? ObjectReference + ) + }) + } catch (e: Throwable) { + return Either.left(e) + } + } + + private fun getName( + context: ExecutionContext, // Execution context to invoke methods + info: ObjectReference, // CoroutineInfo instance + refs: ProcessReferences + ): String { + // equals to `coroutineInfo.context.get(CoroutineName).name` + val coroutineContextInst = context.invokeMethod(info, refs.getContext, emptyList()) as ObjectReference + val coroutineName = context.invokeMethod( + coroutineContextInst, + refs.getContextElement, listOf(refs.nameKey) + ) as? ObjectReference + // If the coroutine doesn't have a given name, CoroutineContext.get(CoroutineName) returns null + val name = if (coroutineName != null) (context.invokeMethod( + coroutineName, + refs.getName, emptyList() + ) as StringReferenceImpl).value() else "coroutine" + val id = (info.getValue(refs.idField) as LongValue).value() + return "$name#$id" + } + + private fun getState( + context: ExecutionContext, // Execution context to invoke methods + info: ObjectReference, // CoroutineInfo instance + refs: ProcessReferences + ): String { + // equals to stringState = coroutineInfo.state.toString() + val state = context.invokeMethod(info, refs.getState, emptyList()) as ObjectReference + return (context.invokeMethod(state, refs.toString, emptyList()) as StringReferenceImpl).value() + } + + private fun getLastObservedThread( + info: ObjectReference, // CoroutineInfo instance + threadRef: Field // reference to lastObservedThread + ): ThreadReference? = info.getValue(threadRef) as ThreadReference? + + /** + * Returns list of stackTraceElements for the given CoroutineInfo's [ObjectReference] + */ + private fun getStackTrace( + info: ObjectReference, + refs: ProcessReferences, + context: ExecutionContext + ): List { + val frameList = context.invokeMethod(info, refs.lastObservedStackTrace, emptyList()) as ObjectReference + val mergedFrameList = context.invokeMethod( + refs.debugProbesImpl, + refs.enhanceStackTraceWithThreadDump, listOf(info, frameList) + ) as ObjectReference + val size = (context.invokeMethod(mergedFrameList, refs.getSize, emptyList()) as IntegerValue).value() + + val list = ArrayList() + for (it in size - 1 downTo 0) { + val frame = context.invokeMethod( + mergedFrameList, refs.getElement, + listOf(context.vm.virtualMachine.mirrorOf(it)) + ) as ObjectReference + val clazz = (frame.getValue(refs.className) as StringReference).value() + +// if (clazz.contains("$DEBUG_PACKAGE.DebugProbes")) break // cut off debug intrinsic stacktrace + list.add( + 0, // add in the beginning + StackTraceElement( + clazz, + (frame.getValue(refs.methodName) as StringReference).value(), + (frame.getValue(refs.fileName) as StringReference?)?.value(), + (frame.getValue(refs.line) as IntegerValue).value() + ) + ) + } + return list + } + + /** + * Holds ClassTypes, Methods, ObjectReferences and Fields for a particular jvm + */ + private class ProcessReferences(context: ExecutionContext) { + // kotlinx.coroutines.debug.DebugProbes instance and methods + val debugProbes = context.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType + val probesImplType = context.findClass("$DEBUG_PACKAGE.internal.DebugProbesImpl") as ClassType + val debugProbesImpl = with(probesImplType) { getValue(fieldByName("INSTANCE")) as ObjectReference } + val enhanceStackTraceWithThreadDump: Method = probesImplType + .methodsByName("enhanceStackTraceWithThreadDump").single() + + val dumpMethod: Method = debugProbes.concreteMethodByName("dumpCoroutinesInfo", "()Ljava/util/List;") + val instance = with(debugProbes) { getValue(fieldByName("INSTANCE")) as ObjectReference } + + // CoroutineInfo + val info = context.findClass("$DEBUG_PACKAGE.CoroutineInfo") as ClassType + val getState: Method = info.concreteMethodByName("getState", "()Lkotlinx/coroutines/debug/State;") + val getContext: Method = info.concreteMethodByName("getContext", "()Lkotlin/coroutines/CoroutineContext;") + val idField: Field = info.fieldByName("sequenceNumber") + val lastObservedStackTrace: Method = info.methodsByName("lastObservedStackTrace").single() + val coroutineContext = context.findClass("kotlin.coroutines.CoroutineContext") as InterfaceType + val getContextElement: Method = coroutineContext.methodsByName("get").single() + val coroutineName = context.findClass("kotlinx.coroutines.CoroutineName") as ClassType + val getName: Method = coroutineName.methodsByName("getName").single() + val nameKey = coroutineName.getValue(coroutineName.fieldByName("Key")) as ObjectReference + val toString: Method = (context.findClass("java.lang.Object") as ClassType) + .methodsByName("toString").single() + + val threadRef: Field = info.fieldByName("lastObservedThread") + val continuation: Field = info.fieldByName("lastObservedFrame") + + // Methods for list + val listType = context.findClass("java.util.List") as InterfaceType + val getSize: Method = listType.methodsByName("size").single() + val getElement: Method = listType.methodsByName("get").single() + val element = context.findClass("java.lang.StackTraceElement") as ClassType + + // for StackTraceElement + val methodName: Field = element.fieldByName("methodName") + val className: Field = element.fieldByName("declaringClass") + val fileName: Field = element.fieldByName("fileName") + val line: Field = element.fieldByName("lineNumber") + } +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebuggerTree.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebuggerTree.kt new file mode 100644 index 00000000000..0172bbda503 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebuggerTree.kt @@ -0,0 +1,444 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines + +import com.intellij.debugger.DebuggerBundle +import com.intellij.debugger.DebuggerInvocationUtil +import com.intellij.debugger.DebuggerManagerEx +import com.intellij.debugger.actions.GotoFrameSourceAction +import com.intellij.debugger.engine.* +import com.intellij.debugger.engine.evaluation.EvaluateException +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.debugger.engine.events.DebuggerCommandImpl +import com.intellij.debugger.engine.events.SuspendContextCommandImpl +import com.intellij.debugger.impl.DebuggerContextImpl +import com.intellij.debugger.impl.DebuggerSession +import com.intellij.debugger.jdi.StackFrameProxyImpl +import com.intellij.debugger.jdi.ThreadReferenceProxyImpl +import com.intellij.debugger.memory.utils.StackFrameItem +import com.intellij.debugger.ui.impl.tree.TreeBuilder +import com.intellij.debugger.ui.impl.tree.TreeBuilderNode +import com.intellij.debugger.ui.impl.watch.* +import com.intellij.debugger.ui.tree.StackFrameDescriptor +import com.intellij.ide.DataManager +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.MessageType +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.ui.DoubleClickListener +import com.intellij.xdebugger.XDebuggerUtil +import com.intellij.xdebugger.XSourcePosition +import com.intellij.xdebugger.frame.XExecutionStack +import com.intellij.xdebugger.impl.XDebuggerManagerImpl +import com.intellij.xdebugger.impl.ui.DebuggerUIUtil +import com.sun.jdi.ClassType +import javaslang.control.Either +import org.jetbrains.kotlin.idea.debugger.KotlinCoroutinesAsyncStackTraceProvider +import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext +import org.jetbrains.kotlin.idea.debugger.evaluate.createExecutionContext +import java.awt.event.MouseEvent +import java.lang.ref.WeakReference +import javax.swing.event.TreeModelEvent +import javax.swing.event.TreeModelListener + +/** + * Tree of coroutines for [CoroutinesPanel] + */ +class CoroutinesDebuggerTree(project: Project) : DebuggerTree(project) { + private val logger = Logger.getInstance(this::class.java) + private var lastSuspendContextDump: Pair, Either>>? = null + + override fun createNodeManager(project: Project): NodeManagerImpl { + return object : NodeManagerImpl(project, this) { + override fun getContextKey(frame: StackFrameProxyImpl?): String? { + return "CoroutinesView" + } + } + } + + /** + * Prepare specific behavior instead of DebuggerTree constructor + */ + init { + val model = object : TreeBuilder(this) { + override fun buildChildren(node: TreeBuilderNode) { + val debuggerTreeNode = node as DebuggerTreeNodeImpl + if (debuggerTreeNode.descriptor is DefaultNodeDescriptor) { + return + } + + node.add(myNodeManager.createMessageNode(MessageDescriptor.EVALUATING)) + buildNode(debuggerTreeNode) + } + + override fun isExpandable(builderNode: TreeBuilderNode): Boolean { + return this@CoroutinesDebuggerTree.isExpandable(builderNode as DebuggerTreeNodeImpl) + } + } + model.setRoot(nodeFactory.defaultNode) + model.addTreeModelListener(createListener()) + + setModel(model) + emptyText.text = "Coroutines are not available" + } + + /** + * Add frames inside coroutine (node) + */ + private fun buildNode(node: DebuggerTreeNodeImpl) { + val context = DebuggerManagerEx.getInstanceEx(project).context + val debugProcess = context.debugProcess + debugProcess?.managerThread?.schedule(object : SuspendContextCommandImpl(context.suspendContext) { + override fun contextAction(suspendContext: SuspendContextImpl) { + val evalContext = debuggerContext.createEvaluationContext() ?: return + if (node.descriptor is CoroutineDescriptorImpl || node.descriptor is CreationFramesDescriptor) { + val children = mutableListOf() + try { + addChildren(children, debugProcess, node.descriptor, evalContext) + } catch (e: EvaluateException) { + children.clear() + children.add(myNodeManager.createMessageNode(e.message)) + logger.debug(e) + } + DebuggerInvocationUtil.swingInvokeLater(project) { + node.removeAllChildren() + for (debuggerTreeNode in children) { + node.add(debuggerTreeNode) + } + node.childrenChanged(true) + } + } + } + }) + } + + fun installAction(): () -> Unit { + val listener = object : DoubleClickListener() { + override fun onDoubleClick(e: MouseEvent): Boolean { + val location = getPathForLocation(e.x, e.y) + ?.lastPathComponent as? DebuggerTreeNodeImpl ?: return false + return selectFrame(location.userObject) + } + } + listener.installOn(this) + + return { listener.uninstall(this) } + } + + fun selectFrame(descriptor: Any): Boolean { + val dataContext = DataManager.getInstance().getDataContext(this@CoroutinesDebuggerTree) + val context = DebuggerManagerEx.getInstanceEx(project).context + when (descriptor) { + is SuspendStackFrameDescriptor -> { + buildSuspendStackFrameChildren(descriptor) + return true + } + is AsyncStackFrameDescriptor -> { + buildAsyncStackFrameChildren(descriptor, context.debugProcess ?: return false) + return true + } + is EmptyStackFrameDescriptor -> { + buildEmptyStackFrameChildren(descriptor) + return true + } + is StackFrameDescriptor -> { + GotoFrameSourceAction.doAction(dataContext) + return true + } + else -> return true + } + } + + private fun buildSuspendStackFrameChildren(descriptor: SuspendStackFrameDescriptor) { + val context = DebuggerManagerEx.getInstanceEx(project).context + val pos = getPosition(descriptor.frame) ?: return + context.debugProcess?.managerThread?.schedule(object : SuspendContextCommandImpl(context.suspendContext) { + override fun contextAction() { + val (stack, stackFrame) = createSyntheticStackFrame(descriptor, pos) ?: return + val action: () -> Unit = { context.debuggerSession?.xDebugSession?.setCurrentStackFrame(stack, stackFrame) } + ApplicationManager.getApplication() + .invokeLater(action, ModalityState.stateForComponent(this@CoroutinesDebuggerTree)) + } + }) + } + + private fun buildAsyncStackFrameChildren(descriptor: AsyncStackFrameDescriptor, process: DebugProcessImpl) { + process.managerThread?.schedule(object : DebuggerCommandImpl() { + override fun action() { + val context = DebuggerManagerEx.getInstanceEx(project).context + val proxy = ThreadReferenceProxyImpl( + process.virtualMachineProxy, + descriptor.state.thread // is not null because it's a running coroutine + ) + val executionStack = JavaExecutionStack(proxy, process, false) + executionStack.initTopFrame() + val frame = descriptor.frame.createFrame(process) + DebuggerUIUtil.invokeLater { + context.debuggerSession?.xDebugSession?.setCurrentStackFrame( + executionStack, + frame + ) + } + } + }) + } + + private fun buildEmptyStackFrameChildren(descriptor: EmptyStackFrameDescriptor) { + val position = getPosition(descriptor.frame) ?: return + val context = DebuggerManagerEx.getInstanceEx(project).context + val suspendContext = context.suspendContext ?: return + val proxy = suspendContext.thread ?: return + context.debugProcess?.managerThread?.schedule(object : DebuggerCommandImpl() { + override fun action() { + val executionStack = + JavaExecutionStack(proxy, context.debugProcess!!, false) + executionStack.initTopFrame() + val frame = SyntheticStackFrame(descriptor, emptyList(), position) + val action: () -> Unit = + { context.debuggerSession?.xDebugSession?.setCurrentStackFrame(executionStack, frame) } + ApplicationManager.getApplication() + .invokeLater(action, ModalityState.stateForComponent(this@CoroutinesDebuggerTree)) + } + }) + } + + private fun getPosition(frame: StackTraceElement): XSourcePosition? { + + val psiFacade = JavaPsiFacade.getInstance(project) + val psiClass = psiFacade.findClass( + frame.className.substringBefore("$"), // find outer class, for which psi exists TODO + GlobalSearchScope.everythingScope(project) + ) + val classFile = psiClass?.containingFile?.virtualFile + return XDebuggerUtil.getInstance().createPosition(classFile, frame.lineNumber) + } + + /** + * Should be invoked on manager thread + */ + private fun createSyntheticStackFrame( + descriptor: SuspendStackFrameDescriptor, + pos: XSourcePosition + ): Pair? { + val context = DebuggerManagerEx.getInstanceEx(project).context + val proxy = context.suspendContext?.thread ?: return null + val executionStack = + JavaExecutionStack(proxy, context.debugProcess!!, false) + executionStack.initTopFrame() + val execContext = context.createExecutionContext() ?: return null + val continuation = descriptor.continuation // guaranteed that it is a BaseContinuationImpl + val aMethod = (continuation.type() as ClassType).concreteMethodByName( + "getStackTraceElement", + "()Ljava/lang/StackTraceElement;" + ) + val debugMetadataKtType = execContext + .findClass("kotlin.coroutines.jvm.internal.DebugMetadataKt") as ClassType + val vars = with(KotlinCoroutinesAsyncStackTraceProvider()) { + KotlinCoroutinesAsyncStackTraceProvider.AsyncStackTraceContext( + execContext, + aMethod, + debugMetadataKtType + ).getSpilledVariables(continuation) + } ?: return null + return executionStack to SyntheticStackFrame(descriptor, vars, pos) + } + + private fun addChildren( + children: MutableList, + debugProcess: DebugProcessImpl, + descriptor: NodeDescriptorImpl, + evalContext: EvaluationContextImpl + ) { + if (descriptor !is CoroutineDescriptorImpl) { + if (descriptor is CreationFramesDescriptor) { + val threadProxy = debuggerContext.suspendContext?.thread ?: return + val proxy = threadProxy.forceFrames().first() + descriptor.frames.forEach { + children.add(myNodeManager.createNode(EmptyStackFrameDescriptor(it, proxy), evalContext)) + } + } + return + } + when (descriptor.state.state) { + CoroutineState.State.RUNNING -> { + if (descriptor.state.thread == null) { + children.add(myNodeManager.createMessageNode("Frames are not available")) + return + } + val proxy = ThreadReferenceProxyImpl( + debugProcess.virtualMachineProxy, + descriptor.state.thread + ) + val frames = proxy.forceFrames() + var i = frames.lastIndex + while (i > 0 && frames[i].location().method().name() != "resumeWith") i-- + // if i is less than 0, wait, what? + for (frame in 0..--i) { + children.add(createFrameDescriptor(descriptor, evalContext, frames[frame])) + } + if (i > 0) { // add async stack trace if there are frames after invokeSuspend + val async = KotlinCoroutinesAsyncStackTraceProvider().getAsyncStackTrace( + JavaStackFrame(StackFrameDescriptorImpl(frames[i - 1], MethodsTracker()), true), + evalContext.suspendContext + ) + async?.forEach { children.add(createAsyncFrameDescriptor(descriptor, evalContext, it, frames[0])) } + } + for (frame in i + 2..frames.lastIndex) { + children.add(createFrameDescriptor(descriptor, evalContext, frames[frame])) + } + } + CoroutineState.State.SUSPENDED -> { + val threadProxy = debuggerContext.suspendContext?.thread ?: return + val proxy = threadProxy.forceFrames().first() + // the thread is paused on breakpoint - it has at least one frame + for (it in descriptor.state.stackTrace) { + if (it.className.startsWith("\b\b\b")) break + children.add(createCoroutineFrameDescriptor(descriptor, evalContext, it, proxy)) + } + } + else -> { + } + } + val trace = descriptor.state.stackTrace + val index = trace.indexOfFirst { it.className.startsWith("\b\b\b") } + children.add(myNodeManager.createNode(CreationFramesDescriptor(trace.subList(index + 1, trace.size)), evalContext)) + } + + + private fun createFrameDescriptor( + descriptor: NodeDescriptorImpl, + evalContext: EvaluationContextImpl, + frame: StackFrameProxyImpl + ): DebuggerTreeNodeImpl { + return myNodeManager.createNode( + myNodeManager.getStackFrameDescriptor(descriptor, frame), + evalContext + ) + } + + private fun createCoroutineFrameDescriptor( + descriptor: CoroutineDescriptorImpl, + evalContext: EvaluationContextImpl, + frame: StackTraceElement, + proxy: StackFrameProxyImpl, + parent: NodeDescriptorImpl? = null + ): DebuggerTreeNodeImpl { + return myNodeManager.createNode( + myNodeManager.getDescriptor( + parent, + CoroutineStackFrameData(descriptor.state, frame, proxy) + ), evalContext + ) + } + + private fun createAsyncFrameDescriptor( + descriptor: CoroutineDescriptorImpl, + evalContext: EvaluationContextImpl, + frame: StackFrameItem, + proxy: StackFrameProxyImpl + ): DebuggerTreeNodeImpl { + return myNodeManager.createNode( + myNodeManager.getDescriptor( + descriptor, + CoroutineStackFrameData(descriptor.state, frame, proxy) + ), evalContext + ) + } + + private fun createListener() = object : TreeModelListener { + override fun treeNodesChanged(event: TreeModelEvent) { + hideTooltip() + } + + override fun treeNodesInserted(event: TreeModelEvent) { + hideTooltip() + } + + override fun treeNodesRemoved(event: TreeModelEvent) { + hideTooltip() + } + + override fun treeStructureChanged(event: TreeModelEvent) { + hideTooltip() + } + } + + override fun isExpandable(node: DebuggerTreeNodeImpl): Boolean { + val descriptor = node.descriptor + return if (descriptor is StackFrameDescriptor) return false else descriptor.isExpandable + } + + override fun build(context: DebuggerContextImpl) { + val session = context.debuggerSession + val command = RefreshCoroutinesTreeCommand(session, context.suspendContext) + + val state = if (session != null) session.state else DebuggerSession.State.DISPOSED + if (ApplicationManager.getApplication().isUnitTestMode + || state == DebuggerSession.State.PAUSED + ) { + showMessage(MessageDescriptor.EVALUATING) + context.debugProcess!!.managerThread.schedule(command) + } else { + showMessage(if (session != null) session.stateDescription else DebuggerBundle.message("status.debug.stopped")) + } + } + + private inner class RefreshCoroutinesTreeCommand(private val mySession: DebuggerSession?, context: SuspendContextImpl?) : + SuspendContextCommandImpl(context) { + + override fun contextAction() { + val root = nodeFactory.defaultNode + mySession ?: return + val suspendContext = suspendContext + if (suspendContext == null || suspendContext.isResumed) { + setRoot(root.apply { add(myNodeManager.createMessageNode("Application is resumed")) }) + return + } + val evaluationContext = EvaluationContextImpl(suspendContext, suspendContext.frameProxy) + val executionContext = ExecutionContext(evaluationContext, suspendContext.frameProxy ?: return) + val cache = lastSuspendContextDump + val states = if (cache != null && cache.first.get() === suspendContext) { + cache.second + } else CoroutinesDebugProbesProxy.dumpCoroutines(executionContext).apply { + lastSuspendContextDump = WeakReference(suspendContext) to this + } + // if suspend context hasn't changed - use last dump, else compute new + if (states.isLeft) { + logger.warn(states.left) + setRoot(root.apply { + clear() + add(nodeFactory.createMessageNode(MessageDescriptor("Dump failed"))) + }) + XDebuggerManagerImpl.NOTIFICATION_GROUP + .createNotification( + "Coroutine dump failed. See log", + MessageType.ERROR + ).notify(project) + return + } + for (state in states.get()) { + root.add( + nodeFactory.createNode( + nodeFactory.getDescriptor(null, CoroutineData(state)), evaluationContext + ) + ) + } + setRoot(root) + } + + private fun setRoot(root: DebuggerTreeNodeImpl) { + DebuggerInvocationUtil.swingInvokeLater(project) { + mutableModel.setRoot(root) + treeChanged() + } + } + + } + +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesPanel.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesPanel.kt new file mode 100644 index 00000000000..c1382c50077 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesPanel.kt @@ -0,0 +1,161 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +@file:Suppress("DEPRECATION") + +package org.jetbrains.kotlin.idea.debugger.coroutines + +import com.intellij.debugger.engine.events.SuspendContextCommandImpl +import com.intellij.debugger.impl.DebuggerContextImpl +import com.intellij.debugger.impl.DebuggerContextListener +import com.intellij.debugger.impl.DebuggerSession +import com.intellij.debugger.impl.DebuggerStateManager +import com.intellij.debugger.ui.impl.DebuggerTreePanel +import com.intellij.debugger.ui.impl.watch.DebuggerTree +import com.intellij.debugger.ui.impl.watch.DebuggerTreeNodeImpl +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionPopupMenu +import com.intellij.openapi.actionSystem.EmptyActionGroup +import com.intellij.openapi.actionSystem.PlatformDataKeys +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.ui.ScrollPaneFactory +import com.intellij.util.Alarm +import org.jetbrains.annotations.NonNls +import java.awt.BorderLayout +import java.awt.event.KeyAdapter +import java.awt.event.KeyEvent +import java.util.* + +/** + * Actually added into ui in [CoroutinesDebugConfigurationExtension.registerCoroutinesPanel] + * Some methods are copied from [com.intellij.debugger.ui.impl.ThreadsPanel] + */ +class CoroutinesPanel(project: Project, stateManager: DebuggerStateManager) : DebuggerTreePanel(project, stateManager) { + private val myUpdateLabelsAlarm = Alarm(Alarm.ThreadToUse.SWING_THREAD) + + init { + val disposable = getCoroutinesTree().installAction() + registerDisposable(disposable) + getCoroutinesTree().addKeyListener(object : KeyAdapter() { + override fun keyPressed(e: KeyEvent) { + if (e.keyCode == KeyEvent.VK_ENTER && getCoroutinesTree().selectionCount == 1) { + val selected = getCoroutinesTree().selectionModel.selectionPath.lastPathComponent + if (selected is DebuggerTreeNodeImpl) getCoroutinesTree().selectFrame(selected.userObject) + } + } + }) + add(ScrollPaneFactory.createScrollPane(getCoroutinesTree()), BorderLayout.CENTER) + stateManager.addListener(object : DebuggerContextListener { + override fun changeEvent(newContext: DebuggerContextImpl, event: DebuggerSession.Event) { + if (DebuggerSession.Event.ATTACHED == event || DebuggerSession.Event.RESUME == event) { + startLabelsUpdate() + } else if (DebuggerSession.Event.PAUSE == event + || DebuggerSession.Event.DETACHED == event + || DebuggerSession.Event.DISPOSE == event + ) { + myUpdateLabelsAlarm.cancelAllRequests() + } + if (DebuggerSession.Event.DETACHED == event || DebuggerSession.Event.DISPOSE == event) { + stateManager.removeListener(this) + } + } + }) + startLabelsUpdate() + } + + private fun startLabelsUpdate() { + if (myUpdateLabelsAlarm.isDisposed) return + myUpdateLabelsAlarm.cancelAllRequests() + myUpdateLabelsAlarm.addRequest(object : Runnable { + override fun run() { + var updateScheduled = false + try { + if (isUpdateEnabled) { + val tree = getCoroutinesTree() + val root = tree.model.root as DebuggerTreeNodeImpl + val process = context.debugProcess + if (process != null) { + process.managerThread.schedule(object : SuspendContextCommandImpl(context.suspendContext) { + override fun contextAction() { + try { + updateNodeLabels(root) + } finally { + reschedule() + } + } + + override fun commandCancelled() { + reschedule() + } + }) + updateScheduled = true + } + } + } finally { + if (!updateScheduled) { + reschedule() + } + } + } + + private fun reschedule() { + val session = context.debuggerSession + if (session != null && session.isAttached && !session.isPaused && !myUpdateLabelsAlarm.isDisposed) { + myUpdateLabelsAlarm.addRequest( + this, + LABELS_UPDATE_DELAY_MS, ModalityState.NON_MODAL + ) + } + } + + }, LABELS_UPDATE_DELAY_MS, ModalityState.NON_MODAL) + } + + override fun dispose() { + Disposer.dispose(myUpdateLabelsAlarm) + super.dispose() + } + + private fun updateNodeLabels(from: DebuggerTreeNodeImpl) { + val children = from.children() + try { + while (children.hasMoreElements()) { + val child = children.nextElement() as DebuggerTreeNodeImpl + child.descriptor.updateRepresentation( + null + ) { child.labelChanged() } + updateNodeLabels(child) + } + } catch (ignored: NoSuchElementException) { // children have changed - just skip + } + + } + + override fun createTreeView(): DebuggerTree { + return CoroutinesDebuggerTree(project) + } + + override fun createPopupMenu(): ActionPopupMenu { + val group = EmptyActionGroup() + return ActionManager.getInstance().createActionPopupMenu("Debugger.CoroutinesPanelPopup", group) + } + + override fun getData(dataId: String): Any? { + return if (PlatformDataKeys.HELP_ID.`is`(dataId)) { + HELP_ID + } else super.getData(dataId) + } + + fun getCoroutinesTree(): CoroutinesDebuggerTree = tree as CoroutinesDebuggerTree + + companion object { + @NonNls + private val HELP_ID = "debugging.debugCoroutines" + private const val LABELS_UPDATE_DELAY_MS = 200 + } + +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/SyntheticStackFrame.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/SyntheticStackFrame.kt new file mode 100644 index 00000000000..8c3fd985ebd --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/SyntheticStackFrame.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines + +import com.intellij.debugger.engine.JavaStackFrame +import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl +import com.intellij.xdebugger.XSourcePosition +import com.intellij.xdebugger.frame.XCompositeNode +import com.intellij.xdebugger.frame.XNamedValue +import com.intellij.xdebugger.frame.XValueChildrenList + +/** + * Puts the frameProxy into JavaStackFrame just to instantiate. SyntheticStackFrame provides it's own data for variables view. + */ +class SyntheticStackFrame( + descriptor: StackFrameDescriptorImpl, + private val vars: List, + private val position: XSourcePosition +) : + JavaStackFrame(descriptor, true) { + + override fun computeChildren(node: XCompositeNode) { + val list = XValueChildrenList() + vars.forEach { list.add(it) } + node.addChildren(list, true) + } + + override fun getSourcePosition(): XSourcePosition? { + return position + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + + val frame = other as? JavaStackFrame ?: return false + + return descriptor.frameProxy == frame.descriptor.frameProxy + } + + override fun hashCode(): Int { + return descriptor.frameProxy.hashCode() + } +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/descriptors.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/descriptors.kt new file mode 100644 index 00000000000..94bf451c6a7 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/descriptors.kt @@ -0,0 +1,222 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines + +import com.intellij.debugger.DebuggerManagerEx +import com.intellij.debugger.engine.SuspendContextImpl +import com.intellij.debugger.engine.evaluation.EvaluateException +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.debugger.impl.DebuggerUtilsEx +import com.intellij.debugger.impl.descriptors.data.DescriptorData +import com.intellij.debugger.impl.descriptors.data.DisplayKey +import com.intellij.debugger.impl.descriptors.data.SimpleDisplayKey +import com.intellij.debugger.jdi.StackFrameProxyImpl +import com.intellij.debugger.memory.utils.StackFrameItem +import com.intellij.debugger.ui.impl.watch.MethodsTracker +import com.intellij.debugger.ui.impl.watch.NodeDescriptorImpl +import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl +import com.intellij.debugger.ui.tree.render.DescriptorLabelListener +import com.intellij.icons.AllIcons +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.IconLoader +import com.sun.jdi.ClassType +import com.sun.jdi.ObjectReference +import org.jetbrains.kotlin.idea.IconExtensionChooser +import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext +import javax.swing.Icon + +/** + * Describes coroutine itself in the tree (name: STATE), has children if stacktrace is not empty (state = CREATED) + */ +class CoroutineData(private val state: CoroutineState) : DescriptorData() { + + override fun createDescriptorImpl(project: Project): CoroutineDescriptorImpl { + return CoroutineDescriptorImpl(state) + } + + override fun equals(other: Any?): Boolean { + return if (other !is CoroutineData) { + false + } else state.name == other.state.name + } + + override fun hashCode(): Int { + return state.name.hashCode() + } + + override fun getDisplayKey(): DisplayKey { + return SimpleDisplayKey(state.name) + } +} + +class CoroutineDescriptorImpl(val state: CoroutineState) : NodeDescriptorImpl() { + var suspendContext: SuspendContextImpl? = null + val 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? { + return state.name + } + + @Throws(EvaluateException::class) + override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener): String { + val name = if (state.thread != null) state.thread.name().substringBefore(" @${state.name}") else "" + val threadState = if (state.thread != null) DebuggerUtilsEx.getThreadStatusText(state.thread.status()) else "" + return "${state.name}: ${state.state}${if (name.isNotEmpty()) " on thread \"$name\":$threadState" else ""}" + } + + override fun isExpandable(): Boolean { + return state.state != CoroutineState.State.CREATED + } + + override fun setContext(context: EvaluationContextImpl?) { + + } +} + +class CoroutineStackFrameData private constructor(val state: CoroutineState, private val proxy: StackFrameProxyImpl) : + DescriptorData() { + private var frame: StackTraceElement? = null + private var frameItem: StackFrameItem? = null + + constructor(state: CoroutineState, frame: StackTraceElement, proxy: StackFrameProxyImpl) : this(state, proxy) { + this.frame = frame + } + + constructor(state: CoroutineState, frameItem: StackFrameItem, proxy: StackFrameProxyImpl) : this(state, proxy) { + this.frameItem = frameItem + } + + override fun hashCode() = frame?.hashCode() ?: frameItem.hashCode() + + override fun equals(other: Any?): Boolean { + return if (other is CoroutineStackFrameData) { + other.frame == frame && other.frameItem == frameItem + } else false + } + + /** + * Returns [EmptyStackFrameDescriptor], [SuspendStackFrameDescriptor] + * or [AsyncStackFrameDescriptor] according to current frame + */ + override fun createDescriptorImpl(project: Project): NodeDescriptorImpl { + val frame = frame ?: return AsyncStackFrameDescriptor( + state, + frameItem!!, + proxy + ) + // check whether last fun is suspend fun + val suspendContext = + DebuggerManagerEx.getInstanceEx(project).context.suspendContext ?: return EmptyStackFrameDescriptor( + frame, + proxy + ) + val suspendProxy = suspendContext.frameProxy ?: return EmptyStackFrameDescriptor( + frame, + proxy + ) + val evalContext = EvaluationContextImpl(suspendContext, suspendContext.frameProxy) + val context = ExecutionContext(evalContext, suspendProxy) + val clazz = context.findClass(frame.className) as ClassType + val method = clazz.methodsByName(frame.methodName).last { + val loc = it.location().lineNumber() + loc < 0 && frame.lineNumber < 0 || loc > 0 && loc <= frame.lineNumber + } // pick correct method if an overloaded one is given + return if ("Lkotlin/coroutines/Continuation;)" in method.signature() || + method.name() == "invokeSuspend" && + method.signature() == "(Ljava/lang/Object;)Ljava/lang/Object;" // suspend fun or invokeSuspend + ) { + val continuation = state.getContinuation(frame, context) + if (continuation == null) EmptyStackFrameDescriptor( + frame, + proxy + ) else + SuspendStackFrameDescriptor( + state, + frame, + proxy, + continuation + ) + } else EmptyStackFrameDescriptor(frame, proxy) + } + + override fun getDisplayKey(): DisplayKey = SimpleDisplayKey(state) +} + +/** + * Descriptor for suspend functions + */ +class SuspendStackFrameDescriptor( + val state: CoroutineState, + val frame: StackTraceElement, + proxy: StackFrameProxyImpl, + val continuation: ObjectReference +) : + StackFrameDescriptorImpl(proxy, MethodsTracker()) { + override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String { + return with(frame) { + val pack = className.substringBeforeLast(".", "") + "$methodName:$lineNumber, ${className.substringAfterLast(".")} " + + if (pack.isNotEmpty()) "{$pack}" else "" + } + } + + override fun isExpandable() = false + + override fun getName(): String { + return frame.methodName + } + + override fun getIcon(): Icon { + return IconLoader.getIcon("org/jetbrains/kotlin/idea/icons/suspendCall.${IconExtensionChooser.iconExtension()}") + } +} + +class AsyncStackFrameDescriptor(val state: CoroutineState, val frame: StackFrameItem, proxy: StackFrameProxyImpl) : + StackFrameDescriptorImpl(proxy, MethodsTracker()) { + override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String { + return with(frame) { + val pack = path().substringBeforeLast(".", "") + "${method()}:${line()}, ${path().substringAfterLast(".")} ${if (pack.isNotEmpty()) "{$pack}" else ""}" + } + } + + override fun getName(): String { + return frame.method() + } + + override fun isExpandable(): Boolean = false +} + +/** + * For the case when no data inside frame is available + */ +class EmptyStackFrameDescriptor(val frame: StackTraceElement, proxy: StackFrameProxyImpl) : + StackFrameDescriptorImpl(proxy, MethodsTracker()) { + override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String { + return with(frame) { + val pack = className.substringBeforeLast(".", "") + "$methodName:$lineNumber, ${className.substringAfterLast(".")} ${if (pack.isNotEmpty()) "{$pack}" else ""}" + } + } + + override fun getName() = null + override fun isExpandable() = false +} + +class CreationFramesDescriptor(val frames: List) : NodeDescriptorImpl() { + override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String { + return "Coroutine creation stack trace" + } + + override fun setContext(context: EvaluationContextImpl?) {} + override fun getName() = "Coroutine creation stack trace" + override fun isExpandable() = true +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-util/src/org/jetbrains/kotlin/idea/debugger/evaluate/ExecutionContext.kt b/idea/jvm-debugger/jvm-debugger-util/src/org/jetbrains/kotlin/idea/debugger/evaluate/ExecutionContext.kt index 98a66fa6ef5..626b33ba204 100644 --- a/idea/jvm-debugger/jvm-debugger-util/src/org/jetbrains/kotlin/idea/debugger/evaluate/ExecutionContext.kt +++ b/idea/jvm-debugger/jvm-debugger-util/src/org/jetbrains/kotlin/idea/debugger/evaluate/ExecutionContext.kt @@ -6,10 +6,12 @@ package org.jetbrains.kotlin.idea.debugger.evaluate import com.intellij.debugger.engine.DebugProcessImpl +import com.intellij.debugger.engine.DebuggerManagerThreadImpl import com.intellij.debugger.engine.SuspendContextImpl import com.intellij.debugger.engine.evaluation.EvaluateException import com.intellij.debugger.engine.evaluation.EvaluateExceptionUtil import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.debugger.impl.DebuggerContextImpl import com.intellij.debugger.impl.DebuggerUtilsEx import com.intellij.debugger.jdi.StackFrameProxyImpl import com.intellij.debugger.jdi.VirtualMachineProxyImpl @@ -98,4 +100,9 @@ class ExecutionContext(val evaluationContext: EvaluationContextImpl, val framePr @Suppress("DEPRECATION") DebuggerUtilsEx.keep(reference, evaluationContext) } +} + +fun DebuggerContextImpl.createExecutionContext(): ExecutionContext? { + val context = createEvaluationContext() + return ExecutionContext(context ?: return null, frameProxy ?: return null) } \ No newline at end of file diff --git a/idea/resources/META-INF/jvm.xml b/idea/resources/META-INF/jvm.xml index 93c68d93cd8..37ce11f2a50 100644 --- a/idea/resources/META-INF/jvm.xml +++ b/idea/resources/META-INF/jvm.xml @@ -60,6 +60,24 @@ + + + + + + + + + + + @@ -103,6 +121,7 @@ + @@ -181,6 +200,10 @@ description="Enable bytecode instrumentation for Kotlin classes" defaultValue="false" restartRequired="false"/> + diff --git a/idea/testData/debugger/tinyApp/src/coroutines/noCoroutines.kt b/idea/testData/debugger/tinyApp/src/coroutines/noCoroutines.kt new file mode 100644 index 00000000000..c855db5ee1f --- /dev/null +++ b/idea/testData/debugger/tinyApp/src/coroutines/noCoroutines.kt @@ -0,0 +1,6 @@ +package noCoroutines + +fun main() { + //Breakpoint! + val s = "nothing to see here, folks" +} \ No newline at end of file diff --git a/idea/testData/debugger/tinyApp/src/coroutines/noCoroutines.out b/idea/testData/debugger/tinyApp/src/coroutines/noCoroutines.out new file mode 100644 index 00000000000..b1080bd756f --- /dev/null +++ b/idea/testData/debugger/tinyApp/src/coroutines/noCoroutines.out @@ -0,0 +1,7 @@ +LineBreakpoint created at noCoroutines.kt:5 +Run Java +Connected to the target VM +noCoroutines.kt:5 +Disconnected from the target VM + +Process finished with exit code 0 diff --git a/idea/testData/debugger/tinyApp/src/coroutines/threeCoroutines.kt b/idea/testData/debugger/tinyApp/src/coroutines/threeCoroutines.kt new file mode 100644 index 00000000000..90f8e9be07f --- /dev/null +++ b/idea/testData/debugger/tinyApp/src/coroutines/threeCoroutines.kt @@ -0,0 +1,13 @@ +package threeCoroutines + +import kotlin.random.Random + +suspend fun main() { + sequence { + yield(239) + sequence { + //Breakpoint! + yield(666) + }.toList() + }.toList() +} diff --git a/idea/testData/debugger/tinyApp/src/coroutines/threeCoroutines.out b/idea/testData/debugger/tinyApp/src/coroutines/threeCoroutines.out new file mode 100644 index 00000000000..110fff9ad6c --- /dev/null +++ b/idea/testData/debugger/tinyApp/src/coroutines/threeCoroutines.out @@ -0,0 +1,10 @@ +LineBreakpoint created at threeCoroutines.kt:10 +Run Java +Connected to the target VM +threeCoroutines.kt:10 +"coroutine#1", state: RUNNING +"coroutine#2", state: RUNNING +"coroutine#3", state: RUNNING +Disconnected from the target VM + +Process finished with exit code 0 diff --git a/idea/testData/debugger/tinyApp/src/coroutines/twoDumps.kt b/idea/testData/debugger/tinyApp/src/coroutines/twoDumps.kt new file mode 100644 index 00000000000..133e6c9b5c2 --- /dev/null +++ b/idea/testData/debugger/tinyApp/src/coroutines/twoDumps.kt @@ -0,0 +1,31 @@ +package twoDumps + +suspend fun main() { + foo() + sequence { + foo() + yield(666) + }.toList() +} + +suspend fun foo() { + val f = 239 + bar() +} + +suspend fun bar() { + var r = 1337 + //Breakpoint! + r += 42 +} + +suspend fun SequenceScope.foo() { + val k = 228 + bar() +} + +suspend fun SequenceScope.bar() { + var r = 1337 + //Breakpoint! + r += 42 +} \ No newline at end of file diff --git a/idea/testData/debugger/tinyApp/src/coroutines/twoDumps.out b/idea/testData/debugger/tinyApp/src/coroutines/twoDumps.out new file mode 100644 index 00000000000..88a1c7d28d6 --- /dev/null +++ b/idea/testData/debugger/tinyApp/src/coroutines/twoDumps.out @@ -0,0 +1,12 @@ +LineBreakpoint created at twoDumps.kt:19 +LineBreakpoint created at twoDumps.kt:30 +Run Java +Connected to the target VM +twoDumps.kt:19 +"coroutine#1", state: RUNNING +twoDumps.kt:30 +"coroutine#1", state: RUNNING +"coroutine#2", state: RUNNING +Disconnected from the target VM + +Process finished with exit code 0 diff --git a/idea/tests/org/jetbrains/kotlin/idea/debugger/coroutines/AbstractCoroutineDumpTest.kt b/idea/tests/org/jetbrains/kotlin/idea/debugger/coroutines/AbstractCoroutineDumpTest.kt new file mode 100644 index 00000000000..4ef897b6fe5 --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/idea/debugger/coroutines/AbstractCoroutineDumpTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines + +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.execution.configurations.JavaParameters +import com.intellij.execution.process.ProcessOutputTypes +import com.intellij.jarRepository.JarRepositoryManager +import com.intellij.jarRepository.RemoteRepositoryDescription +import com.intellij.openapi.util.io.FileUtil +import org.jetbrains.idea.maven.aether.ArtifactKind +import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor +import org.jetbrains.kotlin.idea.debugger.KotlinDebuggerTestBase +import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext +import java.io.File + +abstract class AbstractCoroutineDumpTest : KotlinDebuggerTestBase() { + + + protected fun doTest(path: String) { + val fileText = FileUtil.loadFile(File(path)) + + configureSettings(fileText) + createAdditionalBreakpoints(fileText) + createDebugProcess(path) + + doOnBreakpoint { + val evalContext = EvaluationContextImpl(this, frameProxy) + val execContext = ExecutionContext(evalContext, frameProxy ?: return@doOnBreakpoint) + val either = CoroutinesDebugProbesProxy.dumpCoroutines(execContext) + try { + if (either.isRight) + try { + val states = either.get() + print(stringDump(states), ProcessOutputTypes.SYSTEM) + } catch (ignored: Throwable) { + } + else + throw AssertionError("Dump failed", either.left) + } finally { + resume(this) + } + } + + doOnBreakpoint { + val evalContext = EvaluationContextImpl(this, frameProxy) + val execContext = ExecutionContext(evalContext, frameProxy ?: return@doOnBreakpoint) + val either = CoroutinesDebugProbesProxy.dumpCoroutines(execContext) + try { + if (either.isRight) + try { + val states = either.get() + print(stringDump(states), ProcessOutputTypes.SYSTEM) + } catch (ignored: Throwable) { + } + else + throw AssertionError("Dump failed", either.left) + } finally { + resume(this) + } + } + } + + private fun stringDump(states: List) = buildString { + states.forEach { + appendln("\"${it.name}\", state: ${it.state}") + } + } + + override fun createJavaParameters(mainClass: String?): JavaParameters { + val description = JpsMavenRepositoryLibraryDescriptor("org.jetbrains.kotlinx", "kotlinx-coroutines-debug", "1.3.0") + val debugJar = JarRepositoryManager.loadDependenciesSync( + project, description, setOf(ArtifactKind.ARTIFACT), + RemoteRepositoryDescription.DEFAULT_REPOSITORIES, null + ) ?: throw AssertionError("Debug Dependency is not found") + val params = super.createJavaParameters(mainClass) + for (jar in debugJar) { + params.classPath.add(jar.file.presentableUrl) + if (jar.file.name.contains("kotlinx-coroutines-debug")) + params.vmParametersList.add("-javaagent:${jar.file.presentableUrl}") + } + return params + } +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpTestGenerated.java new file mode 100644 index 00000000000..cc913593d25 --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpTestGenerated.java @@ -0,0 +1,46 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines; + +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.kotlin.test.JUnit3RunnerWithInners; +import org.jetbrains.kotlin.test.KotlinTestUtils; +import org.jetbrains.kotlin.test.TargetBackend; +import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.runner.RunWith; + +import java.io.File; +import java.util.regex.Pattern; + +/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */ +@SuppressWarnings("all") +@TestMetadata("idea/testData/debugger/tinyApp/src/coroutines") +@TestDataPath("$PROJECT_ROOT") +@RunWith(JUnit3RunnerWithInners.class) +public class CoroutineDumpTestGenerated extends AbstractCoroutineDumpTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath); + } + + public void testAllFilesPresentInCoroutines() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/debugger/tinyApp/src/coroutines"), Pattern.compile("^(.+)\\.(kt|kts)$"), TargetBackend.ANY, true); + } + + @TestMetadata("noCoroutines.kt") + public void testNoCoroutines() throws Exception { + runTest("idea/testData/debugger/tinyApp/src/coroutines/noCoroutines.kt"); + } + + @TestMetadata("threeCoroutines.kt") + public void testThreeCoroutines() throws Exception { + runTest("idea/testData/debugger/tinyApp/src/coroutines/threeCoroutines.kt"); + } + + @TestMetadata("twoDumps.kt") + public void testTwoDumps() throws Exception { + runTest("idea/testData/debugger/tinyApp/src/coroutines/twoDumps.kt"); + } +}