From 10472a2795b6985b17382953a308038bcddb5714 Mon Sep 17 00:00:00 2001 From: Maria Sokolova Date: Mon, 6 Jan 2025 18:01:21 +0100 Subject: [PATCH] [coroutines-debugger] Fix the slow Coroutine View: obtain jobs and parents of coroutines with a Helper method. Build the coroutine hierarchy in CoroutineView, from a job and the first parent of each coroutine. IDEA-335303 GitOrigin-RevId: 7fe634a291364438229cfae7680894fba25b07c6 --- .../coroutines/CoroutinesDebugHelper.java | 54 ++++++ .../KotlinDebuggerCoroutinesBundle.properties | 2 +- .../coroutine/command/CoroutineDumpAction.kt | 7 +- .../data/CoroutineStackFramesProvider.kt | 2 +- .../coroutine/data/coroutineInfoDatas.kt | 159 ++++++++++++----- .../proxy/CoroutineDebugProbesProxy.kt | 52 +++++- .../coroutine/proxy/CoroutineInfoProvider.kt | 82 +++------ .../coroutine/util/CoroutineFrameBuilder.kt | 8 +- .../coroutine/view/CoroutineDumpPanel.kt | 26 +-- .../debugger/coroutine/view/CoroutineView.kt | 160 +++++++++--------- .../test/AbstractCoroutineDumpTest.kt | 2 +- 11 files changed, 346 insertions(+), 208 deletions(-) diff --git a/java/java-runtime/src/com/intellij/rt/debugger/coroutines/CoroutinesDebugHelper.java b/java/java-runtime/src/com/intellij/rt/debugger/coroutines/CoroutinesDebugHelper.java index 0a975e8068c3..3cd6684fec58 100644 --- a/java/java-runtime/src/com/intellij/rt/debugger/coroutines/CoroutinesDebugHelper.java +++ b/java/java-runtime/src/com/intellij/rt/debugger/coroutines/CoroutinesDebugHelper.java @@ -11,6 +11,10 @@ public final class CoroutinesDebugHelper { private static final String COROUTINE_OWNER_CLASS = "CoroutineOwner"; private static final String DEBUG_METADATA_FQN = "kotlin.coroutines.jvm.internal.DebugMetadataKt"; private static final String BASE_CONTINUATION_FQN = "kotlin.coroutines.jvm.internal.BaseContinuationImpl"; + private static final String DEBUG_COROUTINE_INFO_FQN = "kotlinx.coroutines.debug.internal.DebugCoroutineInfo"; + private static final String COROUTINE_CONTEXT_FQN = "kotlin.coroutines.CoroutineContext"; + private static final String COROUTINE_JOB_FQN = "kotlinx.coroutines.Job"; + private static final String COROUTINE_CONTEXT_KEY_FQN = "kotlin.coroutines.CoroutineContext$Key"; public static long[] getCoroutinesRunningOnCurrentThread(Object debugProbes, Thread currentThread) throws ReflectiveOperationException { List coroutinesIds = new ArrayList<>(); @@ -140,12 +144,62 @@ public final class CoroutinesDebugHelper { return current.getClass().getSimpleName().contains(COROUTINE_OWNER_CLASS); } + public static Object[] dumpCoroutinesInfoAsJsonAndReferences() throws ReflectiveOperationException { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + Class debugProbesImplClass = classLoader.loadClass("kotlinx.coroutines.debug.internal.DebugProbesImpl"); + Object debugProbesImplInstance = debugProbesImplClass.getField("INSTANCE").get(null); + Object[] infos = (Object[])invoke(debugProbesImplInstance, "dumpCoroutinesInfoAsJsonAndReferences"); + return infos; + } + + /** + * This method takes the array of {@link kotlinx.coroutines.debug.internal.DebugCoroutineInfo} instances + * and for each coroutine requests it's job, and it's first parent. + * + * @return an array of Strings of size (debugCoroutineInfos.size * 2), where + * (2 * i)-th element is a String representation of the job and + * (2 * i + 1)-th element is a String representation of the parent of the i-th coroutine from debugCoroutineInfos array. + */ + public static String[] getJobsAndParentsForCoroutines(Object ... debugCoroutineInfos) throws ReflectiveOperationException { + if (debugCoroutineInfos.length == 0) return new String[]{}; + String[] jobsWithParents = new String[debugCoroutineInfos.length * 2]; + ClassLoader loader = debugCoroutineInfos[0].getClass().getClassLoader(); + Class debugCoroutineInfoClass = Class.forName(DEBUG_COROUTINE_INFO_FQN, false, loader); + Class coroutineContext = Class.forName(COROUTINE_CONTEXT_FQN, false, loader); + Class coroutineContextKey = Class.forName(COROUTINE_CONTEXT_KEY_FQN, false, loader); + Class coroutineJobClass = Class.forName(COROUTINE_JOB_FQN, false, loader); + Object coroutineJobKey = coroutineJobClass.getField("Key").get(null); // Job.Key + Method coroutineContextGet = coroutineContext.getMethod("get", coroutineContextKey); + Method getParentJob = coroutineJobClass.getMethod("getParent"); + Method getContext = debugCoroutineInfoClass.getMethod("getContext"); + + for (int i = 0; i < debugCoroutineInfos.length * 2; i += 2) { + Object info = debugCoroutineInfos[i / 2]; + if (info == null) { + jobsWithParents[i] = null; + jobsWithParents[i + 1] = null; + continue; + } + Object context = invoke(info, getContext); + Object job = invoke(context, coroutineContextGet, coroutineJobKey); + Object parent = invoke(job, getParentJob); + jobsWithParents[i] = (job == null) ? null : job.toString(); + jobsWithParents[i + 1] = (parent == null) ? null : parent.toString(); + } + return jobsWithParents; + } + private static Object getField(Object object, String fieldName) throws ReflectiveOperationException { Field field = object.getClass().getField(fieldName); field.setAccessible(true); return field.get(object); } + private static Object invoke(Object object, Method method, Object... args) throws ReflectiveOperationException { + method.setAccessible(true); + return method.invoke(object, args); + } + private static Object invoke(Object object, String methodName) throws ReflectiveOperationException { Method method = object.getClass().getMethod(methodName); method.setAccessible(true); diff --git a/plugins/kotlin/jvm-debugger/coroutines/resources/messages/KotlinDebuggerCoroutinesBundle.properties b/plugins/kotlin/jvm-debugger/coroutines/resources/messages/KotlinDebuggerCoroutinesBundle.properties index 9a3dcacf3700..e46b561b898c 100644 --- a/plugins/kotlin/jvm-debugger/coroutines/resources/messages/KotlinDebuggerCoroutinesBundle.properties +++ b/plugins/kotlin/jvm-debugger/coroutines/resources/messages/KotlinDebuggerCoroutinesBundle.properties @@ -20,7 +20,7 @@ coroutine.dump.threads.loading=Loading\u2026 coroutine.view.node.root=Coroutines coroutine.view.node.dispatchers=Dispatchers -coroutine.view.node.jobs=Jobs +coroutine.view.node.jobs=Coroutines hierarchy coroutine.view.title=Coroutines coroutine.view.dispatcher.empty=Empty dispatcher coroutine.view.fetching.error=An error occurred on fetching information diff --git a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/command/CoroutineDumpAction.kt b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/command/CoroutineDumpAction.kt index 77b8259c3029..e0aa204dc4cb 100644 --- a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/command/CoroutineDumpAction.kt +++ b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/command/CoroutineDumpAction.kt @@ -23,8 +23,7 @@ import com.intellij.xdebugger.impl.XDebuggerManagerImpl import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.jetbrains.kotlin.idea.debugger.coroutine.KotlinDebuggerCoroutinesBundle -import org.jetbrains.kotlin.idea.debugger.coroutine.data.CompleteCoroutineInfoData -import org.jetbrains.kotlin.idea.debugger.coroutine.data.toCompleteCoroutineInfoData +import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.CoroutineDebugProbesProxy import org.jetbrains.kotlin.idea.debugger.coroutine.view.CoroutineDumpPanel @@ -39,7 +38,7 @@ class CoroutineDumpAction : AnAction() { executeOnDMT(suspendContext) { val states = CoroutineDebugProbesProxy(suspendContext).dumpCoroutines() if (states.isOk()) { - val coroutines = states.cache.map { it.toCompleteCoroutineInfoData() } + val coroutines = states.cache withContext(Dispatchers.EDT) { val ui = session.xDebugSession?.ui ?: return@withContext addCoroutineDump(project, coroutines, ui, session.searchScope) @@ -54,7 +53,7 @@ class CoroutineDumpAction : AnAction() { /** * Analog of [DebuggerUtilsEx.addThreadDump]. */ - fun addCoroutineDump(project: Project, coroutines: List, ui: RunnerLayoutUi, searchScope: GlobalSearchScope) { + 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 diff --git a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/CoroutineStackFramesProvider.kt b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/CoroutineStackFramesProvider.kt index e18c7b52b90f..fd2fdfaeb9fc 100644 --- a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/CoroutineStackFramesProvider.kt +++ b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/CoroutineStackFramesProvider.kt @@ -8,7 +8,7 @@ import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.fetchCoroutineStacksIn class CoroutineStackFramesProvider(private val executionContext: DefaultExecutionContext) { fun fetchCoroutineStackFrames(lastObservedFrame: ObjectReference?): CoroutineStacksInfoData? { - lastObservedFrame ?: return null + if (lastObservedFrame == null) return null return fetchCoroutineStacksInfoData(executionContext, lastObservedFrame) } } diff --git a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/coroutineInfoDatas.kt b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/coroutineInfoDatas.kt index e9131f7898e7..bec95b3f827c 100644 --- a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/coroutineInfoDatas.kt +++ b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/coroutineInfoDatas.kt @@ -4,10 +4,9 @@ package org.jetbrains.kotlin.idea.debugger.coroutine.data import com.intellij.debugger.engine.JavaValue import com.intellij.debugger.engine.SuspendContext +import com.sun.jdi.ObjectReference import com.sun.jdi.ThreadReference import org.jetbrains.annotations.ApiStatus -import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData.Companion.DEFAULT_COROUTINE_NAME -import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData.Companion.DEFAULT_COROUTINE_STATE import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.mirror.MirrorOfCoroutineInfo @ApiStatus.Internal @@ -20,55 +19,132 @@ data class CoroutineStacksInfoData( } } -abstract class CoroutineInfoData(val descriptor: CoroutineDescriptor) { - abstract val continuationStackFrames: List - abstract val creationStackFrames: List - abstract val activeThread: ThreadReference? - abstract val jobHierarchy: List +open class CoroutineInfoData( + name: String?, + val id: Long?, + state: String?, + val dispatcher: String?, + val lastObservedFrame: ObjectReference?, + val lastObservedThread: ThreadReference?, + val debugCoroutineInfoRef: ObjectReference?, + private val stackFrameProvider: CoroutineStackFramesProvider? +) { + val name: String = name ?: DEFAULT_COROUTINE_NAME - fun isSuspended() = descriptor.state == State.SUSPENDED + val state: State = State.fromString(state) - fun isCreated() = descriptor.state == State.CREATED + var job: String? = null - fun isRunning() = descriptor.state == State.RUNNING + var parentJob: String? = null + + private val contextSummary = "[$dispatcher, ${job ?: ""}]" + + val coroutineDescriptor: String by lazy { + "\"$name:$id\": $state $contextSummary" + } + + private val coroutineStackFrames: CoroutineStacksInfoData? by lazy { + stackFrameProvider?.fetchCoroutineStackFrames(lastObservedFrame) + } + + open val continuationStackFrames: List by lazy { + coroutineStackFrames?.continuationStackFrames ?: emptyList() + } + + open val creationStackFrames: List by lazy { + coroutineStackFrames?.creationStackFrames ?: emptyList() + } + + val isSuspended: Boolean = this.state == State.SUSPENDED + + val isRunning: Boolean = this.state == State.RUNNING + + val isCreated: Boolean = this.state == State.CREATED - fun isRunningOnCurrentThread(suspendContext: SuspendContext) = - activeThread == suspendContext.thread?.threadReference + fun isRunningOnCurrentThread(suspendContext: SuspendContext): Boolean = + lastObservedThread == suspendContext.thread?.threadReference companion object { - const val DEFAULT_COROUTINE_NAME = "coroutine" - const val DEFAULT_COROUTINE_STATE = "UNKNOWN" + @Deprecated("This API will not be exposed in the future versions.") + const val DEFAULT_COROUTINE_NAME: String = "coroutine" + @Deprecated("This API will not be exposed in the future versions.") + const val DEFAULT_COROUTINE_STATE: String = "UNKNOWN" } -} - -class LazyCoroutineInfoData( - private val mirror: MirrorOfCoroutineInfo, - private val stackTraceProvider: CoroutineStackFramesProvider, - private val jobHierarchyProvider: CoroutineJobHierarchyProvider -) : CoroutineInfoData(CoroutineDescriptor.instance(mirror)) { - override val creationStackFrames: List by lazy { - stackTraceProvider.getCreationStackTrace(mirror) + @Deprecated("Please use API of CoroutineInfoData instead.") + val descriptor: CoroutineDescriptor by lazy { + CoroutineDescriptor( + name = this.name, + id = id.toString(), + state = this.state, + dispatcher = dispatcher, + contextSummary = contextSummary + ) } - override val continuationStackFrames: List - get() = stackTraceProvider.getContinuationStack(mirror) + @Deprecated("Please use lastObservedThread instead.", ReplaceWith("lastObservedThread")) + val activeThread: ThreadReference? by lazy { lastObservedThread } + + @Deprecated("The hierarchy of parent jobs for a current coroutine is not computed anymore.") + val jobHierarchy: List by lazy { emptyList() } +} + +@ApiStatus.Internal +fun createCoroutineInfoDataFromMirror( + mirror: MirrorOfCoroutineInfo, + stackFrameProvider: CoroutineStackFramesProvider +): CoroutineInfoData = + CoroutineInfoData( + name = mirror.context?.name, + id = mirror.context?.id, + state = mirror.state, + dispatcher = mirror.context?.dispatcher, + lastObservedFrame = mirror.lastObservedFrame, + lastObservedThread = mirror.lastObservedThread, + debugCoroutineInfoRef = null, + stackFrameProvider = stackFrameProvider + ) - override val activeThread = mirror.lastObservedThread +@ApiStatus.Internal +enum class State(val state: String) { + RUNNING("RUNNING"), + SUSPENDED("SUSPENDED"), + CREATED("CREATED"), + UNKNOWN("UNKNOWN"); - override val jobHierarchy by lazy { - jobHierarchyProvider.findJobHierarchy(mirror) + companion object { + fun fromString(state: String?): State { + return entries.find { it.state.equals(state, ignoreCase = true) } ?: UNKNOWN + } } } +@Deprecated("Please use CoroutineInfoData API instead.") class CompleteCoroutineInfoData( descriptor: CoroutineDescriptor, - override val continuationStackFrames: List, - override val creationStackFrames: List, - override val activeThread: ThreadReference? = null, // for suspended coroutines should be null - override val jobHierarchy: List = emptyList() -) : CoroutineInfoData(descriptor) + continuationStackFrames: List, + creationStackFrames: List, + activeThread: ThreadReference? = null, // for suspended coroutines should be null + jobHierarchy: List = emptyList() +) : CoroutineInfoData( + name = descriptor.name, + id = descriptor.id.toLong(), + state = descriptor.state.state, + dispatcher = descriptor.dispatcher, + lastObservedFrame = null, + lastObservedThread = activeThread, + debugCoroutineInfoRef = null, + stackFrameProvider = null +) { + override val continuationStackFrames: List by lazy { + continuationStackFrames + } + override val creationStackFrames: List by lazy { + creationStackFrames + } +} +@Deprecated("Please use CoroutineInfoData API instead.") fun CoroutineInfoData.toCompleteCoroutineInfoData() = when (this) { is CompleteCoroutineInfoData -> this @@ -82,29 +158,18 @@ fun CoroutineInfoData.toCompleteCoroutineInfoData() = ) } +@Deprecated("Please use CoroutineInfoData API instead.") data class CoroutineDescriptor(val name: String, val id: String, val state: State, val dispatcher: String?, val contextSummary: String?) { fun formatName() = "$name:$id" companion object { fun instance(mirror: MirrorOfCoroutineInfo): CoroutineDescriptor = CoroutineDescriptor( - mirror.context?.name ?: DEFAULT_COROUTINE_NAME, + mirror.context?.name ?: CoroutineInfoData.DEFAULT_COROUTINE_NAME, "${mirror.sequenceNumber}", - State.valueOf(mirror.state ?: DEFAULT_COROUTINE_STATE), + State.valueOf(mirror.state ?: CoroutineInfoData.DEFAULT_COROUTINE_STATE), mirror.context?.dispatcher, mirror.context?.summary ) } } - -enum class State { - RUNNING, - SUSPENDED, - CREATED, - UNKNOWN, - SUSPENDED_COMPLETING, - SUSPENDED_CANCELLING, - CANCELLED, - COMPLETED, - NEW -} diff --git a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineDebugProbesProxy.kt b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineDebugProbesProxy.kt index 0eb97d39c4c5..91e8ca7e1244 100644 --- a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineDebugProbesProxy.kt +++ b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineDebugProbesProxy.kt @@ -4,10 +4,16 @@ package org.jetbrains.kotlin.idea.debugger.coroutine.proxy import com.intellij.debugger.engine.DebuggerManagerThreadImpl import com.intellij.debugger.engine.SuspendContextImpl import com.intellij.debugger.impl.DebuggerUtilsImpl.logError +import com.intellij.rt.debugger.coroutines.CoroutinesDebugHelper +import com.sun.jdi.ArrayReference +import com.sun.jdi.StringReference import org.jetbrains.kotlin.idea.debugger.base.util.evaluate.DefaultExecutionContext +import org.jetbrains.kotlin.idea.debugger.coroutine.callMethodFromHelper import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoCache import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.mirror.DebugProbesImpl import org.jetbrains.kotlin.idea.debugger.coroutine.util.executionContext +import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData +import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.mirror.CoroutineInfo class CoroutineDebugProbesProxy(val suspendContext: SuspendContextImpl) { /** @@ -21,8 +27,8 @@ class CoroutineDebugProbesProxy(val suspendContext: SuspendContextImpl) { try { val executionContext = suspendContext.executionContext() ?: return coroutineInfoCache.fail() val libraryAgentProxy = findProvider(executionContext) ?: return coroutineInfoCache.ok() - val infoList = libraryAgentProxy.dumpCoroutinesInfo() - coroutineInfoCache.ok(infoList) + val coroutineInfos = libraryAgentProxy.dumpCoroutinesInfo() ?: emptyList() + coroutineInfoCache.ok(coroutineInfos) } catch (e: Throwable) { logError("Exception is thrown by calling dumpCoroutines.", e) coroutineInfoCache.fail() @@ -30,6 +36,48 @@ class CoroutineDebugProbesProxy(val suspendContext: SuspendContextImpl) { return coroutineInfoCache } + /** + * This method aims to reduce the overhead of obtaining the whole parent hierarchy for every DebugCoroutineInfo. + * It is invoked only when the coroutines hierarchy is requested in the Coroutines View. + * + * The Helper method getJobAndParentForCoroutines returns the array of Strings with size = infos.size * 2 and + * array[2 * i] = info.job + * array[2 * i + 1] = info.parent + * + * The corresponding properties [CoroutineInfoData.job] and [CoroutineInfoData.parentJob] are set to the obtained values. + */ + internal fun fetchAndSetJobsAndParentsForCoroutines(infos: List): Boolean { + val executionContext = suspendContext.executionContext() ?: return false + val debugCoroutineInfos = infos.map { it.debugCoroutineInfoRef } + val array = callMethodFromHelper(CoroutinesDebugHelper::class.java, executionContext, "getJobsAndParentsForCoroutines", debugCoroutineInfos) + val jobsWithParents = (array as? ArrayReference)?.values?.map { (it as StringReference).value() } + ?: fallBackToMirrorFetchJobsAndParentsForCoroutines(executionContext, infos) + if (jobsWithParents.isEmpty()) return false + for (i in 0 until jobsWithParents.size step 2) { + infos[i / 2].job = jobsWithParents[i] + infos[i / 2].parentJob = jobsWithParents[i + 1] + } + return true + } + + private fun fallBackToMirrorFetchJobsAndParentsForCoroutines( + executionContext: DefaultExecutionContext, + infos: List + ): List { + val debugProbesImpl = DebugProbesImpl.instance(executionContext) ?: return emptyList() + if (!debugProbesImpl.isInstalled) return emptyList() + val debugCoroutineInfoImpl = CoroutineInfo.instance(executionContext) ?: return emptyList() + val jobsWithParents = arrayOfNulls(infos.size * 2) + for (i in 0 until jobsWithParents.size step 2) { + val info = infos[i / 2] + val debugCoroutineInfoMirror = debugCoroutineInfoImpl.mirror(info.debugCoroutineInfoRef, executionContext) + val job = debugCoroutineInfoMirror?.context?.job + jobsWithParents[i] = job?.details + jobsWithParents[i + 1] = job?.parent?.getJob()?.details + } + return jobsWithParents.toList() + } + private fun findProvider(executionContext: DefaultExecutionContext): CoroutineInfoProvider? { val debugProbesImpl = DebugProbesImpl.instance(executionContext) return if (debugProbesImpl != null && debugProbesImpl.isInstalled) { diff --git a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineInfoProvider.kt b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineInfoProvider.kt index a22af7cd48f7..44287825d9ef 100644 --- a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineInfoProvider.kt +++ b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineInfoProvider.kt @@ -3,13 +3,14 @@ package org.jetbrains.kotlin.idea.debugger.coroutine.proxy import com.google.gson.Gson -import com.intellij.debugger.engine.DebuggerManagerThreadImpl +import com.intellij.rt.debugger.coroutines.CoroutinesDebugHelper import com.sun.jdi.ArrayReference import com.sun.jdi.ObjectReference import com.sun.jdi.StringReference import com.sun.jdi.ThreadReference import org.jetbrains.annotations.VisibleForTesting import org.jetbrains.kotlin.idea.debugger.base.util.evaluate.DefaultExecutionContext +import org.jetbrains.kotlin.idea.debugger.coroutine.callMethodFromHelper import org.jetbrains.kotlin.idea.debugger.coroutine.data.* import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.mirror.* import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger @@ -20,18 +21,17 @@ sealed interface CoroutineInfoProvider { fun dumpCoroutinesInfo(): List? } -class CoroutinesInfoFromJsonAndReferencesProvider( - private val executionContext: DefaultExecutionContext, - private val debugProbesImpl: DebugProbesImpl +internal class CoroutinesInfoFromJsonAndReferencesProvider( + private val executionContext: DefaultExecutionContext ) : CoroutineInfoProvider { private val stackFramesProvider = CoroutineStackFramesProvider(executionContext) - private val jobHierarchyProvider = CoroutineJobHierarchyProvider() override fun dumpCoroutinesInfo(): List { - val array = debugProbesImpl.dumpCoroutinesInfoAsJsonAndReferences(executionContext) - ?: return emptyList() - val arrayValues = array.values // fetch all values at once + val array = callMethodFromHelper(CoroutinesDebugHelper::class.java, executionContext, "dumpCoroutinesInfoAsJsonAndReferences", emptyList()) + ?: fallbackToOldMirrorDump(executionContext) + + val arrayValues = (array as? ArrayReference)?.values ?: return emptyList() if (arrayValues.size != 4) { error("The result array of 'dumpCoroutinesInfoAsJSONAndReferences' should be of size 4") @@ -56,60 +56,32 @@ class CoroutinesInfoFromJsonAndReferencesProvider( return calculateCoroutineInfoData(coroutinesInfo, coroutineInfoRefs, lastObservedThreadRefs, lastObservedFrameRefs) } + private fun fallbackToOldMirrorDump(executionContext: DefaultExecutionContext): ArrayReference? { + val debugProbesImpl = DebugProbesImpl.instance(executionContext) + return if (debugProbesImpl != null && debugProbesImpl.isInstalled) { + debugProbesImpl.dumpCoroutinesInfoAsJsonAndReferences(executionContext) + } else null + } + private fun calculateCoroutineInfoData( coroutineInfos: Array, coroutineInfoRefs: List, lastObservedThreadRefs: List, lastObservedFrameRefs: List ): List { - val result = mutableListOf() - for ((i, info) in coroutineInfos.withIndex()) { - result.add( - getLazyCoroutineInfoData( - info, - coroutineInfoRefs[i], - lastObservedThreadRefs[i], - lastObservedFrameRefs[i], - stackFramesProvider, - jobHierarchyProvider - ) + return coroutineInfoRefs.mapIndexed { i, ref -> + val info = coroutineInfos[i] + CoroutineInfoData( + name = info.name, + id = info.sequenceNumber, + state = info.state, + dispatcher = info.dispatcher, + lastObservedFrame = lastObservedFrameRefs[i], + lastObservedThread = lastObservedThreadRefs[i], + debugCoroutineInfoRef = ref, + stackFrameProvider = stackFramesProvider ) } - return result - } - - private fun getLazyCoroutineInfoData( - info: CoroutineInfoFromJson, - coroutineInfoRef: ObjectReference, - lastObservedThreadRef: ThreadReference?, - lastObservedFrameRef: ObjectReference?, - stackTraceProvider: CoroutineStackFramesProvider, - jobHierarchyProvider: CoroutineJobHierarchyProvider - ): CoroutineInfoData { - DebuggerManagerThreadImpl.assertIsManagerThread() - - // coroutineInfo is a DebugCoroutineInfo. Need to get coroutineInfoRef.context to pass in to CoroutineContext - val contextRef = CoroutineInfo.instance(executionContext)?.getContextRef(coroutineInfoRef) - val coroutineContextMirror = contextRef?.let { - CoroutineContext(executionContext).fetchMirror(info.name, info.id, info.dispatcher, it, executionContext) - } ?: MirrorOfCoroutineContext( - info.name, - info.id, - info.dispatcher, - null, - null - ) - val coroutineInfoMirror = debugProbesImpl.getCoroutineInfo( - coroutineInfoRef, - executionContext, - coroutineContextMirror, - info.sequenceNumber, - info.state, - lastObservedThreadRef, - lastObservedFrameRef - ) - - return createCoroutineInfoDataFromMirror(coroutineInfoMirror, stackTraceProvider) } private data class CoroutineInfoFromJson( @@ -123,7 +95,7 @@ class CoroutinesInfoFromJsonAndReferencesProvider( companion object { fun instance(executionContext: DefaultExecutionContext, debugProbesImpl: DebugProbesImpl): CoroutinesInfoFromJsonAndReferencesProvider? { if (debugProbesImpl.canDumpCoroutinesInfoAsJsonAndReferences()) { - return CoroutinesInfoFromJsonAndReferencesProvider(executionContext, debugProbesImpl) + return CoroutinesInfoFromJsonAndReferencesProvider(executionContext) } return null } diff --git a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/util/CoroutineFrameBuilder.kt b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/util/CoroutineFrameBuilder.kt index ad28339cd3ad..4bb52256a129 100644 --- a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/util/CoroutineFrameBuilder.kt +++ b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/util/CoroutineFrameBuilder.kt @@ -11,7 +11,7 @@ import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.VisibleForTesting import org.jetbrains.kotlin.idea.debugger.base.util.evaluate.DefaultExecutionContext import org.jetbrains.kotlin.idea.debugger.coroutine.data.* -import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.ContinuationHolder +import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.fetchCoroutineStacksInfoData import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.safeSkipCoroutineStackFrameProxy class CoroutineFrameBuilder { @@ -21,13 +21,13 @@ class CoroutineFrameBuilder { fun build(coroutine: CoroutineInfoData, suspendContext: SuspendContextImpl): CoroutineFrameItemLists? = when { - coroutine.isRunning() -> buildStackFrameForActive(coroutine, suspendContext) - coroutine.isSuspended() -> CoroutineFrameItemLists(coroutine.continuationStackFrames, coroutine.creationStackFrames) + coroutine.isRunning -> buildStackFrameForActive(coroutine, suspendContext) + coroutine.isSuspended -> CoroutineFrameItemLists(coroutine.continuationStackFrames, coroutine.creationStackFrames) else -> null } private fun buildStackFrameForActive(coroutine: CoroutineInfoData, suspendContext: SuspendContextImpl): CoroutineFrameItemLists? { - val activeThread = coroutine.activeThread ?: return null + val activeThread = coroutine.lastObservedThread ?: return null val coroutineStackFrameList = mutableListOf() val threadReferenceProxyImpl = ThreadReferenceProxyImpl(suspendContext.virtualMachineProxy, activeThread) diff --git a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutineDumpPanel.kt b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutineDumpPanel.kt index cf3a115064af..4609a510cd07 100644 --- a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutineDumpPanel.kt +++ b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutineDumpPanel.kt @@ -28,7 +28,7 @@ import com.intellij.ui.components.JBList import com.intellij.unscramble.AnalyzeStacktraceUtil import com.intellij.util.PlatformIcons import org.jetbrains.kotlin.idea.debugger.coroutine.KotlinDebuggerCoroutinesBundle -import org.jetbrains.kotlin.idea.debugger.coroutine.data.CompleteCoroutineInfoData +import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData import java.awt.BorderLayout import java.awt.Color import java.awt.datatransfer.StringSelection @@ -43,10 +43,10 @@ class CoroutineDumpPanel( project: Project, consoleView: ConsoleView, toolbarActions: DefaultActionGroup, - val dump: List + val dump: List ) : JPanel(BorderLayout()), UiDataProvider { private var exporterToTextFile: ExporterToTextFile - private var mergedDump = ArrayList() + private var mergedDump = ArrayList() val filterField = SearchTextField() val filterPanel = JPanel(BorderLayout()) private val coroutinesList = JBList(DefaultListModel()) @@ -72,7 +72,7 @@ class CoroutineDumpPanel( addListSelectionListener { val index = selectedIndex if (index >= 0) { - val selection = model.getElementAt(index) as CompleteCoroutineInfoData + val selection = model.getElementAt(index) as CoroutineInfoData AnalyzeStacktraceUtil.printStacktrace(consoleView, stringStackTrace(selection)) } else { AnalyzeStacktraceUtil.printStacktrace(consoleView, "") @@ -141,7 +141,7 @@ class CoroutineDumpPanel( val states = if (UISettings.getInstance().state.mergeEqualStackTraces) mergedDump else dump for (state in states) { if (StringUtil.containsIgnoreCase(stringStackTrace(state), text) || - StringUtil.containsIgnoreCase(state.descriptor.name, text)) { + StringUtil.containsIgnoreCase(state.name, text)) { model.addElement(state) if (selection === state) { selectedIndex = index @@ -177,9 +177,9 @@ class CoroutineDumpPanel( sink[PlatformDataKeys.EXPORTER_TO_TEXT_FILE] = exporterToTextFile } - private fun getAttributes(infoData: CompleteCoroutineInfoData): SimpleTextAttributes { + private fun getAttributes(infoData: CoroutineInfoData): SimpleTextAttributes { return when { - infoData.isSuspended() -> SimpleTextAttributes.GRAY_ATTRIBUTES + infoData.isSuspended -> SimpleTextAttributes.GRAY_ATTRIBUTES infoData.continuationStackFrames.isEmpty() -> SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, Color.GRAY.brighter()) else -> SimpleTextAttributes.REGULAR_ATTRIBUTES } @@ -189,8 +189,8 @@ class CoroutineDumpPanel( @Suppress("HardCodedStringLiteral") override fun customizeCellRenderer(list: JList<*>, value: Any, index: Int, selected: Boolean, hasFocus: Boolean) { - val infoData = value as CompleteCoroutineInfoData - val state = infoData.descriptor + val infoData = value as CoroutineInfoData + val state = infoData icon = fromState(state.state, false) val attrs = getAttributes(infoData) append(state.name + " (", attrs) @@ -246,7 +246,7 @@ class CoroutineDumpPanel( override fun getActionUpdateThread() = ActionUpdateThread.BGT } - private class CopyToClipboardAction(private val myCoroutinesDump: List, private val myProject: Project) : + private class CopyToClipboardAction(private val myCoroutinesDump: List, private val myProject: Project) : DumbAwareAction( KotlinDebuggerCoroutinesBundle.message("coroutine.dump.copy.action"), KotlinDebuggerCoroutinesBundle.message("coroutine.dump.copy.description"), @@ -272,7 +272,7 @@ class CoroutineDumpPanel( private class MyToFileExporter( private val myProject: Project, - private val infoData: List + private val infoData: List ) : ExporterToTextFile { override fun getReportText() = buildString { @@ -288,9 +288,9 @@ class CoroutineDumpPanel( } } -private fun stringStackTrace(info: CompleteCoroutineInfoData) = +private fun stringStackTrace(info: CoroutineInfoData) = buildString { - appendLine("\"${info.descriptor.name}\", state: ${info.descriptor.state}") + appendLine("\"${info.name}\", state: ${info.state}") info.continuationStackFrames.forEach { append("\t") append(ThreadDumpAction.renderLocation(it.location)) diff --git a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutineView.kt b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutineView.kt index 28bf1e9c9bfa..a1d08a07c42c 100644 --- a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutineView.kt +++ b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutineView.kt @@ -142,7 +142,7 @@ internal class CoroutineView(project: Project, javaDebugProcess: JavaDebugProces } val children = XValueChildrenList() if (Registry.`is`("coroutine.panel.show.jobs.hierarchy")) { - children.add(JobsContainer(suspendContext)) + children.add(RootCoroutineContainer(suspendContext)) } else { children.add(DispatchersContainer(suspendContext)) } @@ -150,48 +150,56 @@ internal class CoroutineView(project: Project, javaDebugProcess: JavaDebugProces } } - inner class JobsContainer(val suspendContext: SuspendContextImpl) : + inner class RootCoroutineContainer(val suspendContext: SuspendContextImpl) : RendererContainer(renderer.renderNoIconNode(KotlinDebuggerCoroutinesBundle.message("coroutine.view.node.jobs"))) { override fun computeChildren(node: XCompositeNode) { invokeInSuspendContext(suspendContext) { suspendContext -> - val coroutineCache = CoroutineDebugProbesProxy(suspendContext).dumpCoroutines() + val coroutineDebugProxy = CoroutineDebugProbesProxy(suspendContext) + val coroutineCache = coroutineDebugProxy.dumpCoroutines() if (!coroutineCache.isOk()) { node.addChildren(XValueChildrenList.singleton(ErrorNode("coroutine.view.fetching.error")), true) return@invokeInSuspendContext } - - val jobNodes = mutableMapOf() - val jobs = XValueChildrenList() val coroutines = XValueChildrenList() val cache = coroutineCache.cache - val jobToCoroutineInfo = cache - .filter { it.jobHierarchy.isNotEmpty() } - .associateBy({ it.jobHierarchy.first() }, { it }) - cache.forEach { coroutine -> - val isCurrent = coroutine.isRunningOnCurrentThread(suspendContext) - if (coroutine.jobHierarchy.isNotEmpty()) { - var parent: JobContainer? = null - coroutine.jobHierarchy.reversed().forEach { - parent = jobNodes.computeIfAbsent(it) { jobDetails -> - val coroutineName = jobToCoroutineInfo[jobDetails]?.descriptor?.formatName() - val jobName = (if (coroutineName != null) "\"$coroutineName\":" else "") + jobDetails - JobContainer(suspendContext, jobName, isCurrent).also { jobContainer -> - if (parent == null) { - jobs.add(jobContainer) - } else { - parent!!.addJob(jobContainer) - } - } - } - } - jobNodes[coroutine.jobHierarchy[0]]!!.addCoroutine(coroutine) - } else { - coroutines.add(FramesContainer(coroutine, suspendContext, isCurrent, "")) + val isHierarchyBuilt = coroutineDebugProxy.fetchAndSetJobsAndParentsForCoroutines(cache) + if (isHierarchyBuilt) { + val parentJobToChildCoroutines = cache.groupBy { it.parentJob } + val jobToCoroutine = cache.filter { it.job != null }.associateBy { it.job!! } + val rootCoroutines = parentJobToChildCoroutines.keys.mapNotNull { + val parentJob = jobToCoroutine[it] + // Root coroutines are those with no parent or whose parent is not in the jobToCoroutineInfo map + // E.g. if a coroutine's parent is a BlockingCoroutine, which was not captured in the coroutine dump, because it's already completing. + if (parentJob?.parentJob == null || jobToCoroutine[parentJob.parentJob] == null) { + parentJob + } else null + } + for (rootCoroutine in rootCoroutines) { + coroutines.add( + CoroutineContainer( + suspendContext = suspendContext, + coroutineInfo = rootCoroutine, + isCurrent = rootCoroutine.isRunningOnCurrentThread(suspendContext), + childCoroutines = parentJobToChildCoroutines[rootCoroutine.job] ?: emptyList(), + parentJobToChildCoroutines = parentJobToChildCoroutines + ) + ) + } + } else { + // If the job hierarchy was not fetched, add all the dumped coroutines in the plain view. + for (coroutine in cache) { + coroutines.add( + CoroutineContainer( + suspendContext = suspendContext, + coroutineInfo = coroutine, + isCurrent = coroutine.isRunningOnCurrentThread(suspendContext), + childCoroutines = emptyList(), + parentJobToChildCoroutines = emptyMap() + ) + ) } } - - if (jobs.size() + coroutines.size() > 0) { - node.addChildren(jobs, true) + if (coroutines.size() > 0) { node.addChildren(coroutines, true) } else { node.addChildren(XValueChildrenList.singleton(InfoNode("coroutine.view.fetching.not_found")), true) @@ -200,34 +208,24 @@ internal class CoroutineView(project: Project, javaDebugProcess: JavaDebugProces } } - inner class JobContainer( - private val suspendContext: SuspendContextImpl, - private val jobName: String, - isCurrent: Boolean - ) : RendererContainer(renderer.renderThreadGroup(jobName, isCurrent)) { - private val jobs = mutableListOf() - private val coroutines = mutableListOf() - - fun addJob(jobContainer: JobContainer) { - jobs.add(jobContainer) - } - - fun addCoroutine(coroutine: CoroutineInfoData) { - coroutines.add(coroutine) - } + inner class DispatchersContainer(val suspendContext: SuspendContextImpl) : + RendererContainer(renderer.renderNoIconNode(KotlinDebuggerCoroutinesBundle.message("coroutine.view.node.dispatchers"))) { override fun computeChildren(node: XCompositeNode) { - node.setAlreadySorted(true) invokeInSuspendContext(suspendContext) { suspendContext -> - val children = XValueChildrenList() - - jobs.forEach { - children.add(it) + val coroutineCache = CoroutineDebugProbesProxy(suspendContext).dumpCoroutines() + if (!coroutineCache.isOk()) { + node.addChildren(XValueChildrenList.singleton(ErrorNode("coroutine.view.fetching.error")), true) + return@invokeInSuspendContext } - coroutines.forEach { - val isCurrent = it.isRunningOnCurrentThread(suspendContext) - children.add(FramesContainer(it, suspendContext, isCurrent, jobName)) + val children = XValueChildrenList() + val groups = coroutineCache.cache.groupBy { it.dispatcher } + for (dispatcher in groups.keys) { + // Mark the group that contains a running coroutine with a tick + val coroutines = groups[dispatcher] + val isCurrent = coroutines?.any { it.isRunningOnCurrentThread(suspendContext) } ?: false + children.add(DispatcherContainer(suspendContext, dispatcher ?: EMPTY_DISPATCHER_NAME, isCurrent, coroutines)) } if (children.size() > 0) { @@ -239,25 +237,18 @@ internal class CoroutineView(project: Project, javaDebugProcess: JavaDebugProces } } - inner class DispatchersContainer(val suspendContext: SuspendContextImpl) : - RendererContainer(renderer.renderNoIconNode(KotlinDebuggerCoroutinesBundle.message("coroutine.view.node.dispatchers"))) { + inner class DispatcherContainer( + private val suspendContext: SuspendContextImpl, + dispatcherName: String, + isCurrent: Boolean, + private val coroutines: List? + ) : RendererContainer(renderer.renderThreadGroup(dispatcherName, isCurrent)) { override fun computeChildren(node: XCompositeNode) { invokeInSuspendContext(suspendContext) { suspendContext -> - val coroutineCache = CoroutineDebugProbesProxy(suspendContext).dumpCoroutines() - if (!coroutineCache.isOk()) { - node.addChildren(XValueChildrenList.singleton(ErrorNode("coroutine.view.fetching.error")), true) - return@invokeInSuspendContext - } - val children = XValueChildrenList() - val groups = coroutineCache.cache.groupBy { it.descriptor.dispatcher } - for (dispatcher in groups.keys) { - // Mark the group that contains a running coroutine with a tick - val coroutines = groups[dispatcher] - val isCurrent = coroutines?.any { it.isRunningOnCurrentThread(suspendContext) } ?: false - children.add(CoroutineContainer(suspendContext, dispatcher ?: EMPTY_DISPATCHER_NAME, isCurrent, coroutines)) + coroutines?.forEach { + children.add(CoroutineContainer(suspendContext, it, it.isRunningOnCurrentThread(suspendContext), emptyList(), emptyMap())) } - if (children.size() > 0) { node.addChildren(children, true) } else { @@ -269,16 +260,27 @@ internal class CoroutineView(project: Project, javaDebugProcess: JavaDebugProces inner class CoroutineContainer( private val suspendContext: SuspendContextImpl, - private val groupName: String, + private val coroutineInfo: CoroutineInfoData, isCurrent: Boolean, - private val coroutines: List? - ) : RendererContainer(renderer.renderThreadGroup(groupName, isCurrent)) { + private val childCoroutines: List, + private val parentJobToChildCoroutines: Map> + ) : RendererContainer(renderer.renderThreadGroup(coroutineInfo.coroutineDescriptor, isCurrent)) { override fun computeChildren(node: XCompositeNode) { invokeInSuspendContext(suspendContext) { suspendContext -> val children = XValueChildrenList() - coroutines?.forEach { - val isCurrent = it.isRunningOnCurrentThread(suspendContext) - children.add(FramesContainer(it, suspendContext, isCurrent, groupName)) + children.add(FramesContainer(coroutineInfo, suspendContext)) + + childCoroutines.forEach { + val childCoroutines = parentJobToChildCoroutines[it.job] + children.add( + CoroutineContainer( + suspendContext, + it, + it.isRunningOnCurrentThread(suspendContext), + childCoroutines ?: emptyList(), + parentJobToChildCoroutines + ) + ) } if (children.size() > 0) { node.addChildren(children, true) @@ -295,10 +297,8 @@ internal class CoroutineView(project: Project, javaDebugProcess: JavaDebugProces inner class FramesContainer( private val infoData: CoroutineInfoData, - private val suspendContext: SuspendContextImpl, - isCurrent: Boolean, - parentGroupNameToHideFromContext: String, - ) : RendererContainer(renderer.render(infoData, isCurrent, parentGroupNameToHideFromContext)) { + private val suspendContext: SuspendContextImpl + ) : RendererContainer(renderer.renderNoIconNode("Stacktrace")) { override fun computeChildren(node: XCompositeNode) { node.setAlreadySorted(true) diff --git a/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractCoroutineDumpTest.kt b/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractCoroutineDumpTest.kt index 4793d9e7f54b..390dca3fdf93 100644 --- a/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractCoroutineDumpTest.kt +++ b/plugins/kotlin/jvm-debugger/test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractCoroutineDumpTest.kt @@ -34,7 +34,7 @@ abstract class AbstractCoroutineDumpTest : KotlinDescriptorTestCaseWithStackFram private fun stringDump(infoData: List) = buildString { infoData.forEach { - appendLine("\"${it.descriptor.name}#${it.descriptor.id}\", state: ${it.descriptor.state}") + appendLine("\"${it.name}#${it.id}\", state: ${it.state}") } } }