[debugger] Provide async stack traces for all running coroutines.

The new method com.intellij.rt.debugger.agent.CaptureStorage#getAllCapturedStacks introduced in the debugger-agent allows to obtain captured stack traces for all the threads.

Async stack traces for coroutines are computed in the same evaluation as the dump.

IDEA-366087

GitOrigin-RevId: be52ccd3b391c347536daef0e2567554024bdae3
This commit is contained in:
Maria Sokolova
2025-06-26 12:17:07 +00:00
committed by intellij-monorepo-bot
parent e9e52e34cd
commit 9d49a47f1d
9 changed files with 146 additions and 56 deletions
@@ -17,10 +17,12 @@ public final class CoroutinesDebugHelper {
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";
private static final String DEBUGGER_AGENT_CAPTURE_STORAGE_FQN = "com.intellij.rt.debugger.agent.CaptureStorage";
public static long[] getCoroutinesRunningOnCurrentThread(Object debugProbes, Thread currentThread) throws ReflectiveOperationException {
public static long[] getCoroutinesRunningOnCurrentThread(Class<?> debugProbesImplClass, Thread currentThread) throws ReflectiveOperationException {
Object debugProbesImplInstance = debugProbesImplClass.getField("INSTANCE").get(null);
List<Long> coroutinesIds = new ArrayList<>();
List infos = (List)invoke(debugProbes, "dumpCoroutinesInfo");
List infos = (List)invoke(debugProbesImplInstance, "dumpCoroutinesInfo");
for (Object info : infos) {
if (invoke(info, "getLastObservedThread") == currentThread) {
coroutinesIds.add((Long)invoke(info, "getSequenceNumber"));
@@ -154,10 +156,8 @@ public final class CoroutinesDebugHelper {
return current.getClass().getSimpleName().contains(COROUTINE_OWNER_CLASS);
}
public static Object[] dumpCoroutinesInfoAsJsonAndReferences() throws ReflectiveOperationException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
public static Object[] dumpCoroutinesInfoAsJsonAndReferences(Class<?> debugProbesImplClass) {
try {
Class<?> debugProbesImplClass = classLoader.loadClass("kotlinx.coroutines.debug.internal.DebugProbesImpl");
Object debugProbesImplInstance = debugProbesImplClass.getField("INSTANCE").get(null);
Object[] infos = (Object[])invoke(debugProbesImplInstance, "dumpCoroutinesInfoAsJsonAndReferences");
return infos;
@@ -166,10 +166,8 @@ public final class CoroutinesDebugHelper {
}
}
public static Object[] dumpCoroutinesWithStacktracesAsJson() throws ReflectiveOperationException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
public static Object[] dumpCoroutinesWithStacktracesAsJson(Class<?> debugProbesImplClass) {
try {
Class<?> debugProbesImplClass = classLoader.loadClass("kotlinx.coroutines.debug.internal.DebugProbesImpl");
Object debugProbesImplInstance = debugProbesImplClass.getField("INSTANCE").get(null);
Object[] dump = (Object[])invoke(debugProbesImplInstance, "dumpCoroutinesInfoAsJsonAndReferences");
Object[] coroutineInfos = (Object[])dump[3];
@@ -177,19 +175,50 @@ public final class CoroutinesDebugHelper {
for (int i = 0; i < coroutineInfos.length; i++) {
lastObservedStackTraces[i] = lastObservedStackTrace(coroutineInfos[i]);
}
dump = Arrays.copyOf(dump, dump.length + 1);
dump = Arrays.copyOf(dump, dump.length + 2);
dump[4] = lastObservedStackTraces;
Object[] lastObservedThreads = (Object[])dump[1];
dump[5] = getAsyncStackTracesForThreads(lastObservedThreads);
return dump;
} catch (Throwable e) {
return null;
}
}
public static String lastObservedStackTrace(Object debugCoroutineInfo) throws ReflectiveOperationException {
private static String lastObservedStackTrace(Object debugCoroutineInfo) throws ReflectiveOperationException {
List<StackTraceElement> stackTrace = (List<StackTraceElement>)invoke(debugCoroutineInfo, "lastObservedStackTrace");
return JsonUtils.dumpStackTraceElements(stackTrace);
}
/**
* Invokes com.intellij.rt.debugger.agent.CaptureStorage#getAllCapturedStacks method
* which returns a map of threads to their captured async stack traces.
* If `debugger.async.stack.trace.for.all.threads` is false, only the current thread's stack trace is returned.
*
* If debugger-agent is not available, e.g. in attach, returns null
*/
private static String[] getAsyncStackTracesForThreads(Object[] threads) {
try {
Class<?> captureStorageClass = Class.forName(DEBUGGER_AGENT_CAPTURE_STORAGE_FQN, false, null);
Method getAllCapturedStacks = captureStorageClass.getMethod("getAllCapturedStacks", int.class);
Map<Thread, String> threadToStackTrace = (Map<Thread, String>)invoke(null, getAllCapturedStacks, 500);
String[] asyncStackTraces = new String[threads.length];
for (int i = 0; i < threads.length; i++) {
Object thread = threads[i];
if (thread != null) {
asyncStackTraces[i] = threadToStackTrace.get(thread);
}
}
return asyncStackTraces;
} catch (Throwable e) {
return null;
}
}
/**
* This method takes the array of {@link kotlinx.coroutines.debug.internal.DebugCoroutineInfo} instances
* and for each coroutine finds it's job and the first parent, which corresponds to some coroutine, captured in the dump.