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 4d17db3204db..19c137725134 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 @@ -8,6 +8,7 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -276,6 +277,64 @@ public final class CoroutinesDebugHelper { return jobToCapturedParent; } + /** + * Returns an Object array containing coroutine job hierarchy information, or {@code null} if something went wrong + * or job corresponding to some coroutine does not exist. + *
    + *
  1. {@code String[]} - string representations of the {@code kotlinx.coroutines.Job Job} extracted + * from each coroutine's context, in the same order as the input array.
  2. + *
  3. {@code Object[]} - the actual {@code kotlinx.coroutines.Job Job} objects corresponding to each coroutine.
  4. + *
  5. {@code int[]} - for each job, the index of its nearest ancestor job that is also present + * in this dump, or -1 if no such parent exists.
  6. + *
+ */ + public static Object[] getCoroutineJobHierarchyInfo(Object ... debugCoroutineInfos) throws ReflectiveOperationException { + if (debugCoroutineInfos.length == 0) return new String[]{}; + 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"); + + String[] jobNames = new String[debugCoroutineInfos.length]; + Object[] jobObjects = new Object[debugCoroutineInfos.length]; + int[] parentIndexes = new int[debugCoroutineInfos.length]; + Arrays.fill(parentIndexes, -1); + + for (int i = 0; i < debugCoroutineInfos.length; i++) { + Object info = debugCoroutineInfos[i]; + Object context = invoke(info, getContext); + Object job = invoke(context, coroutineContextGet, coroutineJobKey); + // job corresponding to the coroutine should not be null, coroutines should be dumped again, fast return + if (job == null) return null; + jobNames[i] = job.toString(); + jobObjects[i] = job; + } + // we use this set of coroutine jobs to only save parent jobs which correspond to some coroutine in the dump + Map jobToIndex = new HashMap<>(); + for (int i = 0; i < jobObjects.length; i++) { + Object obj = jobObjects[i]; + jobToIndex.put(obj, i); + } + + for (int i = 0; i < debugCoroutineInfos.length; i++) { + Object job = jobObjects[i]; + Object parent = invoke(job, getParentJob); + while (parent != null) { + if (jobToIndex.containsKey(parent)) { + parentIndexes[i] = jobToIndex.get(parent); + break; + } + parent = invoke(parent, getParentJob); + } + } + return new Object[]{jobNames, jobObjects, parentIndexes}; + } + private static Object getField(Object object, String fieldName) throws ReflectiveOperationException { Field field = object.getClass().getField(fieldName); field.setAccessible(true); 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 24e3ebc7abe6..ab73bd6c71cf 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 @@ -38,6 +38,11 @@ open class CoroutineInfoData( var job: String? = null + var jobId: Long? = null + + var parentJobId: Long? = null + + @Deprecated("Do not use parentJob String as an id to group coroutines, use parentJobId instead.") var parentJob: String? = null // NOTE: dispatchers may have a custom String representation, see IDEA-371498 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 4b0f24eb582f..fe7875e71493 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 @@ -6,6 +6,8 @@ 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.IntegerValue +import com.sun.jdi.ObjectReference import com.sun.jdi.StringReference import org.jetbrains.annotations.ApiStatus import org.jetbrains.kotlin.idea.debugger.base.util.evaluate.DefaultExecutionContext @@ -39,6 +41,15 @@ class CoroutineDebugProbesProxy(val suspendContext: SuspendContextImpl) { return coroutineInfoCache } + @Synchronized + fun dumpCoroutinesWithHierarchy(): CoroutineInfoCache { + val cache = dumpCoroutines() + val infos = cache.cache + if (cache.cache.isEmpty()) return cache + fetchAndSetJobNamesAndJobUniqueIds(infos) + return cache + } + /** * 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. @@ -67,6 +78,28 @@ class CoroutineDebugProbesProxy(val suspendContext: SuspendContextImpl) { return true } + @ApiStatus.Internal + fun fetchAndSetJobNamesAndJobUniqueIds(infos: List): Boolean { + val executionContext = suspendContext.executionContext() ?: return false + val debugCoroutineInfos = infos.map { it.debugCoroutineInfoRef } + val array = callMethodFromHelper(CoroutinesDebugHelper::class.java, executionContext, "getCoroutineJobHierarchyInfo", debugCoroutineInfos) + if (array == null) return false + val jobNames = ((array as ArrayReference).values[0] as ArrayReference).values.map { (it as StringReference).value() } + val jobRefs = (array.values[1] as ArrayReference).values.map { (it as ObjectReference) } + val parentIndexes = (array.values[2] as ArrayReference).values.map { (it as IntegerValue).value() } + for ((i, info) in infos.withIndex()) { + info.job = jobNames[i] + info.jobId = jobRefs[i].uniqueID() + val parentIndex = parentIndexes[i] + if (parentIndex == -1) { + info.parentJobId = null + } else { + info.parentJobId = jobRefs[parentIndex].uniqueID() + } + } + return true + } + private fun fallBackToMirrorFetchJobsAndParentsForCoroutines( executionContext: DefaultExecutionContext, infos: List