[debugger] Compute last observed stack trace for coroutines in one evaluation with the dump.

* It's faster than computing coroutine stack trace for each coroutine separately.
* Coroutine dump will be evaluated together with stack traces even if the vm threads were resumed (by ThreadBlockedMonitor e.g.).
* lastObservedStackTrace is not a lazy property of DebugCoroutineInfo, so it's already computed on the library side when the dump is taken.

GitOrigin-RevId: 07c41bbe1e2682b431aedb6ffc70ba8331cf1765
This commit is contained in:
Maria Sokolova
2025-06-10 18:10:53 +02:00
committed by intellij-monorepo-bot
parent 8fcaef61ba
commit fbd38fbf3b
6 changed files with 97 additions and 4 deletions

View File

@@ -23,6 +23,17 @@ public final class JsonUtils {
return result.toString();
}
public static String dumpStackTraceElements(List<StackTraceElement> stack) {
StringBuilder result = new StringBuilder();
result.append("[");
for (int i = 0; i < stack.size(); i++) {
if (i > 0) result.append(", ");
dumpStackTraceElement(result, stack.get(i));
}
result.append("]");
return result.toString();
}
public static String escapeJsonString(String s) {
StringBuilder builder = new StringBuilder();
int length = s.length();

View File

@@ -166,6 +166,29 @@ public final class CoroutinesDebugHelper {
}
}
public static Object[] dumpCoroutinesWithStacktracesAsJson() throws ReflectiveOperationException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
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];
String[] lastObservedStackTraces = new String[coroutineInfos.length];
for (int i = 0; i < coroutineInfos.length; i++) {
lastObservedStackTraces[i] = lastObservedStackTrace(coroutineInfos[i]);
}
dump[3] = lastObservedStackTraces;
return dump;
} catch (Throwable e) {
return null;
}
}
public static String lastObservedStackTrace(Object debugCoroutineInfo) throws ReflectiveOperationException {
List<StackTraceElement> stackTrace = (List<StackTraceElement>)invoke(debugCoroutineInfo, "lastObservedStackTrace");
return JsonUtils.dumpStackTraceElements(stackTrace);
}
/**
* 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.

View File

@@ -4,6 +4,7 @@ package org.jetbrains.kotlin.idea.debugger.coroutine.data
import com.intellij.debugger.engine.JavaValue
import com.intellij.debugger.engine.SuspendContext
import com.sun.jdi.Location
import com.sun.jdi.ObjectReference
import com.sun.jdi.ThreadReference
import org.jetbrains.annotations.ApiStatus
@@ -27,7 +28,8 @@ open class CoroutineInfoData(
val lastObservedFrame: ObjectReference?,
val lastObservedThread: ThreadReference?,
val debugCoroutineInfoRef: ObjectReference?,
private val stackFrameProvider: CoroutineStackFramesProvider?
private val stackFrameProvider: CoroutineStackFramesProvider?,
val lastObservedStackTrace: List<Location> = emptyList()
) {
val name: String = name ?: DEFAULT_COROUTINE_NAME

View File

@@ -27,7 +27,7 @@ class CoroutineDebugProbesProxy(val suspendContext: SuspendContextImpl) {
try {
val executionContext = suspendContext.executionContext()
val coroutineInfos =
CoroutinesInfoFromJsonAndReferencesProvider(executionContext).dumpCoroutinesInfo()
CoroutinesInfoFromJsonAndReferencesProvider(executionContext).dumpCoroutinesWithStacktraces()
?: CoroutineLibraryAgent2Proxy.instance(executionContext)?.dumpCoroutinesInfo()
?: emptyList()
coroutineInfoCache.ok(coroutineInfos)

View File

@@ -3,8 +3,10 @@
package org.jetbrains.kotlin.idea.debugger.coroutine.proxy
import com.google.gson.Gson
import com.intellij.rt.debugger.JsonUtils
import com.intellij.rt.debugger.coroutines.CoroutinesDebugHelper
import com.sun.jdi.ArrayReference
import com.sun.jdi.Location
import com.sun.jdi.ObjectReference
import com.sun.jdi.StringReference
import com.sun.jdi.ThreadReference
@@ -56,6 +58,40 @@ internal class CoroutinesInfoFromJsonAndReferencesProvider(
return calculateCoroutineInfoData(coroutinesInfo, coroutineInfoRefs, lastObservedThreadRefs, lastObservedFrameRefs)
}
fun dumpCoroutinesWithStacktraces(): List<CoroutineInfoData>? {
val array = callMethodFromHelper(CoroutinesDebugHelper::class.java, executionContext, "dumpCoroutinesWithStacktracesAsJson", emptyList(), JsonUtils::class.java.name)
val arrayValues = (array as? ArrayReference)?.values ?: return null
if (arrayValues.size != 4) {
error("The result array of 'dumpCoroutinesWithStacktracesAsJson' should be of size 4")
}
val coroutinesInfoAsJsonString = arrayValues[0].safeAs<StringReference>()?.value()
?: error("The first element of the result array must be a string")
val lastObservedThreadRefs = arrayValues[1].safeAs<ArrayReference>()?.toTypedList<ThreadReference?>()
?: error("The second element of the result array must be an array")
val lastObservedFrameRefs = arrayValues[2].safeAs<ArrayReference>()?.toTypedList<ObjectReference?>()
?: error("The third element of the result array must be an array")
val lastObservedStackTraceJsons = arrayValues[3].safeAs<ArrayReference>()?.toTypedList<StringReference>()
?: error("The fourth element of the result array must be an array")
val coroutinesInfo = Gson().fromJson(coroutinesInfoAsJsonString, Array<CoroutineInfoFromJson>::class.java)
val lastObservedStackTraces: List<List<Location>> = lastObservedStackTraceJsons.map {
Gson().fromJson(it.value(), Array<StackTraceElementData>::class.java).map { ste ->
findOrCreateLocation(executionContext, ste.stackTraceElement())
}
}
if (lastObservedStackTraces.size != lastObservedFrameRefs.size ||
lastObservedFrameRefs.size != coroutinesInfo.size ||
coroutinesInfo.size != lastObservedThreadRefs.size) {
error("Arrays must have equal sizes")
}
return calculateCoroutineInfoDataWithStacktraces(coroutinesInfo, lastObservedThreadRefs, lastObservedFrameRefs, lastObservedStackTraces)
}
private fun fallbackToOldMirrorDump(executionContext: DefaultExecutionContext): ArrayReference? {
val debugProbesImpl = DebugProbesImpl.instance(executionContext)
return if (debugProbesImpl != null && debugProbesImpl.isInstalled && debugProbesImpl.canDumpCoroutinesInfoAsJsonAndReferences()) {
@@ -84,6 +120,27 @@ internal class CoroutinesInfoFromJsonAndReferencesProvider(
}
}
private fun calculateCoroutineInfoDataWithStacktraces(
coroutineInfos: Array<CoroutineInfoFromJson>,
lastObservedThreadRefs: List<ThreadReference?>,
lastObservedFrameRefs: List<ObjectReference?>,
lastObservedStackTraces: List<List<Location>>
): List<CoroutineInfoData> {
return coroutineInfos.mapIndexed { i, info ->
CoroutineInfoData(
name = info.name,
id = info.sequenceNumber,
state = info.state,
dispatcher = info.dispatcher,
lastObservedFrame = lastObservedFrameRefs[i],
lastObservedThread = lastObservedThreadRefs[i],
debugCoroutineInfoRef = null,
stackFrameProvider = null,
lastObservedStackTrace = lastObservedStackTraces[i]
)
}
}
private data class CoroutineInfoFromJson(
val name: String?,
val id: Long?,

View File

@@ -60,10 +60,10 @@ private class CoroutineDumpItem(info: CoroutineInfoData) : MergeableDumpItem {
override val stackTrace: String =
info.coroutineDescriptor + "\n" +
info.continuationStackFrames.map { it.location }.joinToString(prefix = "\t", separator = "\n") { ThreadDumpAction.renderLocation(it) }
info.lastObservedStackTrace.joinToString(prefix = "\t", separator = "\n\t") { ThreadDumpAction.renderLocation(it) }
override val interestLevel: Int = when {
info.continuationStackFrames.isEmpty() -> -10
info.lastObservedStackTrace.isEmpty() -> -10
else -> stackTrace.count { it == '\n' }
}