[debugger] Introduced provider of Java virtual and platform threads.

Flag: `debugger.thread.dump.include.virtual.threads`

IDEA-355724

GitOrigin-RevId: 5c260df1d4cd2003b17ff8dd7692eec823faa3fb
This commit is contained in:
Maria Sokolova
2025-02-17 22:04:36 +00:00
committed by intellij-monorepo-bot
parent 5b5bd49445
commit 53e920e5e7
6 changed files with 262 additions and 15 deletions
@@ -2,31 +2,44 @@
package com.intellij.debugger.actions
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.engine.DebuggerUtils
import com.intellij.debugger.engine.executeOnDMT
import com.intellij.debugger.engine.*
import com.intellij.debugger.engine.MethodInvokeUtils.getMethodHandlesImplLookup
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.impl.DebuggerUtilsImpl
import com.intellij.debugger.impl.ExtendedThreadDumpItemsProvider
import com.intellij.debugger.jdi.VirtualMachineProxyImpl
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.EDT
import com.intellij.openapi.diagnostic.ControlFlowException
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.util.registry.Registry
import com.intellij.rt.debugger.VirtualThreadDumper
import com.intellij.threadDumpParser.ThreadDumpParser
import com.intellij.threadDumpParser.ThreadState
import com.intellij.unscramble.DumpItem
import com.intellij.unscramble.JavaThreadDumpItem
import com.intellij.util.lang.JavaVersion
import com.jetbrains.jdi.ThreadReferenceImpl
import com.sun.jdi.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.NonNls
import java.lang.Long
import java.util.concurrent.CancellationException
import kotlin.Boolean
import kotlin.Int
import kotlin.Pair
import kotlin.String
import kotlin.Throwable
import kotlin.checkNotNull
import kotlin.let
import kotlin.time.Duration.Companion.seconds
import kotlin.to
class ThreadDumpAction : DumbAwareAction() {
@@ -79,8 +92,35 @@ class ThreadDumpAction : DumbAwareAction() {
"(" + DebuggerUtilsEx.getSourceName(location, "Unknown Source") + ":" + DebuggerUtilsEx.getLineNumber(location, false) + ")"
}
private fun buildThreadDump(context: DebuggerContextImpl): List<DumpItem> {
return buildJavaPlatformThreadDump(context).map(::JavaThreadDumpItem)
private suspend fun buildThreadDump(context: DebuggerContextImpl): List<DumpItem> {
fun fallback() =
buildJavaPlatformThreadDump(context).map(::JavaThreadDumpItem)
if (!Registry.`is`("debugger.thread.dump.extended")) {
return fallback()
}
val timeout = Registry.intValue("debugger.thread.dump.suspension.timeout.seconds", 5).seconds
return try {
suspendAllAndEvaluate(context, timeout) {
fetchExtendedThreadDumpItems(it)
}
}
catch (e: Throwable) {
when (e) {
is TimeoutCancellationException -> {
thisLogger().warn("timeout while waiting for evaluatable context ($timeout)")
fallback()
}
is CancellationException, is ControlFlowException -> {
throw e
}
else -> {
thisLogger().error(e)
fallback()
}
}
}
}
fun buildJavaPlatformThreadDump(context: DebuggerContextImpl): List<ThreadState> {
@@ -96,6 +136,10 @@ class ThreadDumpAction : DumbAwareAction() {
}
}
private fun fetchExtendedThreadDumpItems(suspendContext: SuspendContextImpl): List<DumpItem> =
ExtendedThreadDumpItemsProvider.EP.extensionList
.flatMap { it.compute(suspendContext) }
private fun renderLockedObject(monitor: ObjectReference): String {
return "locked " + renderObject(monitor)
}
@@ -336,7 +380,7 @@ private fun getPlatformThreadsWithStackTraces(vmProxy: VirtualMachineProxyImpl):
if (this.isNotEmpty()) {
append('\n')
}
append('\t')
append("\t ")
try {
append(ThreadDumpAction.renderLocation(stackFrame.location()))
}
@@ -346,4 +390,59 @@ private fun getPlatformThreadsWithStackTraces(vmProxy: VirtualMachineProxyImpl):
}
}
}
}
private class JavaThreadsProvider : ExtendedThreadDumpItemsProvider() {
override val isEnabled: Boolean
get() = Registry.`is`("debugger.thread.dump.include.virtual.threads")
override fun compute(suspendContext: SuspendContextImpl): List<DumpItem> {
val virtualThreads = evaluateAndGetAllVirtualThreads(suspendContext)
val vm = suspendContext.virtualMachineProxy
return buildThreadStates(vm, virtualThreads)
.map(::JavaThreadDumpItem)
}
private fun evaluateAndGetAllVirtualThreads(suspendContext: SuspendContextImpl): List<Pair<ThreadReference, String>> {
if (!Registry.`is`("debugger.thread.dump.include.virtual.threads")) return emptyList()
val version = JavaVersion.parse(suspendContext.virtualMachineProxy.version())
if (version.feature < 19) return emptyList()
val evaluationContext = EvaluationContextImpl(suspendContext, suspendContext.getFrameProxy())
val lookupImpl = getMethodHandlesImplLookup(evaluationContext)
if (lookupImpl == null) {
thisLogger().error("Cannot get MethodHandles.Lookup.IMPL_LOOKUP")
return emptyList()
}
val evaluated = try {
DebuggerUtilsImpl.invokeHelperMethod(
evaluationContext,
VirtualThreadDumper::class.java, "getAllVirtualThreadsWithStackTraces",
listOf(lookupImpl)
)
}
catch (e: EvaluateException) {
thisLogger().error(e)
return emptyList()
}
val packedThreadsAndStackTraces = (evaluated as ArrayReference?)?.values ?: emptyList()
return buildList {
var i = 0
while (i < packedThreadsAndStackTraces.size) {
val stackTrace = (packedThreadsAndStackTraces[i++] as StringReference).value()
while (true) {
val thread = packedThreadsAndStackTraces[i++]
if (thread == null) {
break
}
add(thread as ThreadReference to stackTrace)
}
}
}
}
}
@@ -61,6 +61,15 @@ object MethodInvokeUtils {
.map { "\tat ${DebuggerUtils.getValueAsString(evaluationContext, it)}" }
.joinToString(separator = "\n", postfix = "\n")
}
fun getMethodHandlesImplLookup(evaluationContext: EvaluationContextImpl): ObjectReference? {
val theClass = evaluationContext.debugProcess.findClass(evaluationContext,
"java.lang.invoke.MethodHandles\$Lookup",
null)
val theField = DebuggerUtils.findField(theClass,
"IMPL_LOOKUP")
return theClass?.getValue(theField) as? ObjectReference
}
}
@Throws(EvaluateException::class)
@@ -96,13 +105,11 @@ internal fun tryInvokeWithHelper(
val debugProcess = evaluationContext.debugProcess
val invokerArgs = mutableListOf<Value?>()
val lookupClass =
debugProcess.findClass(evaluationContext, "java.lang.invoke.MethodHandles\$Lookup", evaluationContext.getClassLoader())
if (lookupClass == null) {
logger<MethodInvokeUtils>().error("Lookup class not found, java version " + evaluationContext.virtualMachineProxy.version())
val implLookup = MethodInvokeUtils.getMethodHandlesImplLookup(evaluationContext)
if (implLookup == null) {
logger<MethodInvokeUtils>().error("Cannot get MethodHandles.Lookup.IMPL_LOOKUP, java version " + evaluationContext.virtualMachineProxy.version())
return InvocationResult(false, null)
}
val implLookup = lookupClass.getValue(DebuggerUtils.findField(lookupClass, "IMPL_LOOKUP")) as ObjectReference
invokerArgs.add(implLookup) // lookup
invokerArgs.add(type.classObject()) // class
@@ -162,11 +162,7 @@ class JavaThreadDumpItem(private val threadState: ThreadState) : DumpItem {
private inner class JavaMergeableToken : MergeableToken {
private val comparableStackTrace: String =
stackTrace
.lineSequence()
.drop(min(stackTrace.length, 1))
.map { it.replace("<0x.+>\\s".toRegex(), "<merged>") }
.joinToString("\n")
stackTrace.substringAfter("\n").replace("<0x.+>\\s".toRegex(), "<merged>")
override val item: JavaThreadDumpItem get() = this@JavaThreadDumpItem
@@ -0,0 +1,138 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.rt.debugger;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.*;
@SuppressWarnings("unchecked")
public final class VirtualThreadDumper {
static volatile boolean initialized = false;
static boolean successfully = false;
static MethodHandle streamIteratorHandle;
static MethodHandle containersRootHandle;
static MethodHandle containerChildrenHandle;
static MethodHandle containerThreadsHandle;
static MethodHandle threadIsVirtualHandle;
private static boolean init(MethodHandles.Lookup lookup) {
if (!initialized) {
try {
Class<?> streamClass = Class.forName("java.util.stream.Stream");
streamIteratorHandle = lookup.findVirtual(streamClass, "iterator", MethodType.methodType(Iterator.class));
Class<?> threadContainersClass = Class.forName("jdk.internal.vm.ThreadContainers");
Class<?> threadContainerClass = Class.forName("jdk.internal.vm.ThreadContainer");
containersRootHandle = lookup.findStatic(threadContainersClass, "root", MethodType.methodType(threadContainerClass));
containerChildrenHandle = lookup.findVirtual(threadContainerClass, "children", MethodType.methodType(streamClass));
containerThreadsHandle = lookup.findVirtual(threadContainerClass, "threads", MethodType.methodType(streamClass));
//noinspection JavaLangInvokeHandleSignature
threadIsVirtualHandle = lookup.findVirtual(Thread.class, "isVirtual", MethodType.methodType(boolean.class));
successfully = true;
} catch (NoSuchMethodException | IllegalAccessException | ClassNotFoundException e) {
successfully = false;
}
initialized = true;
}
return successfully;
}
/**
* Returns all virtual threads with stack traces.
* <br/>
* They are grouped by equal stack traces and packed into the plain `Object` array in the following way:
* <ul>
* <li>First, there is the stack trace object as `String`.</li>
* <li>Then, there are one or many thread objects as `Thread` references and each of them has the above stack trace.</li>
* <li>After the last thread object, there is a single `null` as a delimiter.</li>
* <li>Then we have a new group of stack trace and threads, or the array ends.</li>
* </ul>
* <br/>
* Returns an empty array if there are no virtual threads or some error occurred.
*/
public static Object[] getAllVirtualThreadsWithStackTraces(MethodHandles.Lookup lookup) throws Throwable {
if (!init(lookup)) {
return null;
}
ArrayList<Thread> threads = getAllVirtualThreads(lookup);
if (threads.isEmpty()) {
return null;
}
HashMap<String, ArrayList<Thread>> groupedByStackTrace = new HashMap<>();
for (Thread t : threads) {
StringBuilder buffer = new StringBuilder();
for (StackTraceElement ste : t.getStackTrace()) {
buffer.append("\tat ").append(ste).append('\n');
}
String stackTrace = buffer.toString();
ArrayList<Thread> similarThreads = groupedByStackTrace.get(stackTrace);
if (similarThreads == null) {
similarThreads = new ArrayList<>();
groupedByStackTrace.put(stackTrace, similarThreads);
}
similarThreads.add(t);
}
Object[] allStackTraceAndThreads = new Object[threads.size() + groupedByStackTrace.size() * 2];
int i = 0;
for (Map.Entry<String, ArrayList<Thread>> e : groupedByStackTrace.entrySet()) {
String st = e.getKey();
ArrayList<Thread> ts = e.getValue();
allStackTraceAndThreads[i++] = st;
for (Thread t : ts) {
allStackTraceAndThreads[i++] = t;
}
allStackTraceAndThreads[i++] = null;
}
assert i == allStackTraceAndThreads.length;
return allStackTraceAndThreads;
}
private static ArrayList<Thread> getAllVirtualThreads(MethodHandles.Lookup lookup) throws Throwable {
if (!init(lookup)) return null;
ArrayList<Thread> result = new ArrayList<>();
for (Object container : getAllContainers()) {
Object /*Stream<Thread>*/ threads = containerThreadsHandle.invoke(container);
Iterator<Thread> it = (Iterator<Thread>)streamIteratorHandle.invoke(threads);
while (it.hasNext()) {
Thread t = it.next();
boolean isVirtual = (boolean) threadIsVirtualHandle.invoke(t);
if (isVirtual) {
result.add(t);
}
}
}
return result;
}
private static ArrayList<Object> getAllContainers() throws Throwable {
ArrayList<Object> allContainers = new ArrayList<>();
Object rootContainer = containersRootHandle.invoke();
collectContainers(allContainers, rootContainer);
return allContainers;
}
private static void collectContainers(ArrayList<Object> allContainers, Object container) throws Throwable {
allContainers.add(container);
Object/*Stream<ThreadContainer>*/ children = containerChildrenHandle.invoke(container);
Iterator<Object> it = (Iterator<Object>)streamIteratorHandle.invoke(children);
while (it.hasNext()) {
Object/*ThreadContainer*/ child = it.next();
collectContainers(allContainers, child);
}
}
}
@@ -669,6 +669,12 @@ debugger.jb.jdi=true
debugger.jb.jdi.description=Use the new forked JDI implementation
debugger.jdwp.include.virtual.threads=false
debugger.jdwp.include.virtual.threads.description=JDWP setting to include virtual threads in the list of all threads (might lead to problems in case of a huge number of virtual threads)
debugger.thread.dump.extended=false
debugger.thread.dump.extended.description=Try to get extended thread dump (virtual threads, coroutines, ...) during debugger's Get Thread Dump action
debugger.thread.dump.suspension.timeout.seconds=5
debugger.thread.dump.suspension.timeout.seconds.description=Timeout for Get Thread Dump action to wait until we suspend the VM to perform extended dump collection (virtual threads, coroutines, ...)
debugger.thread.dump.include.virtual.threads=true
debugger.thread.dump.include.virtual.threads.description=Try to get virtual threads during debugger's Get Thread Dump action
debugger.async.jdi=true
debugger.async.jdi.description=Use async JDI to speed up JDWP communication
debugger.async.frames=true
@@ -34,6 +34,7 @@
<extensions defaultExtensionNs="com.intellij">
<debugger.creationStackTraceProvider implementation="org.jetbrains.kotlin.idea.debugger.coroutine.CoroutineAsyncStackTraceProvider"/>
<debugger.dumpItemsProvider implementation="org.jetbrains.kotlin.idea.debugger.coroutine.view.CoroutinesDumpAsyncProvider"/>
<debugger.dumpItemsProvider implementation="com.intellij.debugger.actions.JavaThreadsProvider"/>
<runConfigurationExtension implementation="org.jetbrains.kotlin.idea.debugger.coroutine.CoroutineDebugConfigurationExtension"/>