IDEA-384931 [debugger]: Extract job hierarchy for CoroutineInfoData relying on the uniqueId of Job object rather than its string representation

GitOrigin-RevId: 2f05173bc71bc2ac9ad41af35ab081bd7823352e
This commit is contained in:
Maria Sokolova
2026-02-16 23:07:47 +00:00
committed by intellij-monorepo-bot
parent 1f79479346
commit 088fedd031
3 changed files with 97 additions and 0 deletions
@@ -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.
* <ol>
* <li> {@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.</li>
* <li> {@code Object[]} - the actual {@code kotlinx.coroutines.Job Job} objects corresponding to each coroutine.</li>
* <li> {@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.</li>
* </ol>
*/
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<Object, Integer> 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);
@@ -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
@@ -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<CoroutineInfoData>): 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<CoroutineInfoData>