[debugger] Coroutine View: fixed coroutine job hierarchy.

Bugs: IDEA-371866, IDEA-366085

Only show the jobs corresponding to the coroutines captured in the dump.

GitOrigin-RevId: c163cdcdc9c4ba4d1e25a01daa940ff174af263b
This commit is contained in:
Maria Sokolova
2025-05-01 16:26:56 +02:00
committed by intellij-monorepo-bot
parent 35763ae892
commit fd088c015e
3 changed files with 32 additions and 26 deletions

View File

@@ -5,9 +5,7 @@ import com.intellij.rt.debugger.JsonUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.*;
public final class CoroutinesDebugHelper {
@@ -170,7 +168,9 @@ public final class CoroutinesDebugHelper {
/**
* 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.
* and for each coroutine finds it's job and the first parent, which corresponds to some coroutine, captured in the dump.
* That means that parent jobs corresponding to ScopeCoroutines (coroutineScope) or DispatchedCoroutine (withContext)
* will be skipped. Their frames will be seen in the async stack trace.
*
* @return an array of Strings of size (debugCoroutineInfos.size * 2), where
* (2 * i)-th element is a String representation of the job and
@@ -178,7 +178,6 @@ public final class CoroutinesDebugHelper {
*/
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);
@@ -189,20 +188,33 @@ public final class CoroutinesDebugHelper {
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;
}
String[] jobToCapturedParent = new String[debugCoroutineInfos.length * 2];
Set<String> capturedJobs = new HashSet<>();
for(Object info : debugCoroutineInfos) {
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();
capturedJobs.add(job.toString());
}
return jobsWithParents;
for (int i = 0; i < debugCoroutineInfos.length * 2; i += 2) {
Object info = debugCoroutineInfos[i / 2];
Object context = invoke(info, getContext);
Object job = invoke(context, coroutineContextGet, coroutineJobKey);
if (job == null) {
jobToCapturedParent[i] = null;
jobToCapturedParent[i + 1] = null;
continue;
}
jobToCapturedParent[i] = job.toString();
Object parent = invoke(job, getParentJob);
while (parent != null) {
if (capturedJobs.contains(parent.toString())) {
jobToCapturedParent[i + 1] = parent.toString();
break;
}
parent = invoke(parent, getParentJob);
}
}
return jobToCapturedParent;
}
private static Object getField(Object object, String fieldName) throws ReflectiveOperationException {

View File

@@ -46,6 +46,9 @@ class CoroutineDebugProbesProxy(val suspendContext: SuspendContextImpl) {
* array[2 * i] = info.job
* array[2 * i + 1] = info.parent
*
* NOTE: only coroutines which are captured in the coroutine dump will be shown in the Coroutine View,
* jobs which do not correspond to any captured coroutine will not be shown (e.g., jobs of completing coroutines or scope coroutines).
*
* The corresponding properties [CoroutineInfoData.job] and [CoroutineInfoData.parentJob] are set to the obtained values.
*/
internal fun fetchAndSetJobsAndParentsForCoroutines(infos: List<CoroutineInfoData>): Boolean {

View File

@@ -183,16 +183,7 @@ internal class CoroutineView(project: Project, javaDebugProcess: JavaDebugProces
if (isHierarchyBuilt) {
val parentJobToChildCoroutineInfos = cache.groupBy { it.parentJob }
val jobToCoroutineInfo = cache.associateBy { it.job }
val parentJobs = parentJobToChildCoroutineInfos.keys
val rootJobs = parentJobs.mapNotNull {
// The root job's coroutine is either not present in the dump (and in jobToCoroutineInfo map)
// (e.g. if it's a BlockingCoroutine, which was not captured in the coroutine dump, because it's already completing).
// or it has no parent.
val parentCoroutineInfo = jobToCoroutineInfo[it]
if (parentCoroutineInfo == null || parentCoroutineInfo.parentJob == null) {
it
} else null
}
val rootJobs = cache.filter { it.parentJob == null }.mapNotNull { it.job }
for (rootJob in rootJobs) {
val rootCoroutine = jobToCoroutineInfo[rootJob]
coroutines.add(