diff --git a/java/debugger/impl/src/com/intellij/debugger/actions/ThreadDumpAction.kt b/java/debugger/impl/src/com/intellij/debugger/actions/ThreadDumpAction.kt index 3d2324c93707..e3a2d27c1643 100644 --- a/java/debugger/impl/src/com/intellij/debugger/actions/ThreadDumpAction.kt +++ b/java/debugger/impl/src/com/intellij/debugger/actions/ThreadDumpAction.kt @@ -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 { - return buildJavaPlatformThreadDump(context).map(::JavaThreadDumpItem) + private suspend fun buildThreadDump(context: DebuggerContextImpl): List { + 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 { @@ -96,6 +136,10 @@ class ThreadDumpAction : DumbAwareAction() { } } +private fun fetchExtendedThreadDumpItems(suspendContext: SuspendContextImpl): List = + 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 { + val virtualThreads = evaluateAndGetAllVirtualThreads(suspendContext) + + val vm = suspendContext.virtualMachineProxy + return buildThreadStates(vm, virtualThreads) + .map(::JavaThreadDumpItem) + } + + private fun evaluateAndGetAllVirtualThreads(suspendContext: SuspendContextImpl): List> { + 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) + } + } + } + } } \ No newline at end of file diff --git a/java/debugger/impl/src/com/intellij/debugger/engine/MethodInvokeUtils.kt b/java/debugger/impl/src/com/intellij/debugger/engine/MethodInvokeUtils.kt index 8ed409d6454a..0d3f2e7c2f48 100644 --- a/java/debugger/impl/src/com/intellij/debugger/engine/MethodInvokeUtils.kt +++ b/java/debugger/impl/src/com/intellij/debugger/engine/MethodInvokeUtils.kt @@ -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() - val lookupClass = - debugProcess.findClass(evaluationContext, "java.lang.invoke.MethodHandles\$Lookup", evaluationContext.getClassLoader()) - if (lookupClass == null) { - logger().error("Lookup class not found, java version " + evaluationContext.virtualMachineProxy.version()) + val implLookup = MethodInvokeUtils.getMethodHandlesImplLookup(evaluationContext) + if (implLookup == null) { + logger().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 diff --git a/java/java-impl/src/com/intellij/unscramble/DumpItem.kt b/java/java-impl/src/com/intellij/unscramble/DumpItem.kt index 95b0f5a848ea..0a383b8c66a2 100644 --- a/java/java-impl/src/com/intellij/unscramble/DumpItem.kt +++ b/java/java-impl/src/com/intellij/unscramble/DumpItem.kt @@ -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(), "") } - .joinToString("\n") + stackTrace.substringAfter("\n").replace("<0x.+>\\s".toRegex(), "") override val item: JavaThreadDumpItem get() = this@JavaThreadDumpItem diff --git a/java/java-runtime/src/com/intellij/rt/debugger/VirtualThreadDumper.java b/java/java-runtime/src/com/intellij/rt/debugger/VirtualThreadDumper.java new file mode 100644 index 000000000000..0bde38599bd8 --- /dev/null +++ b/java/java-runtime/src/com/intellij/rt/debugger/VirtualThreadDumper.java @@ -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. + *
+ * They are grouped by equal stack traces and packed into the plain `Object` array in the following way: + *
    + *
  • First, there is the stack trace object as `String`.
  • + *
  • Then, there are one or many thread objects as `Thread` references and each of them has the above stack trace.
  • + *
  • After the last thread object, there is a single `null` as a delimiter.
  • + *
  • Then we have a new group of stack trace and threads, or the array ends.
  • + *
+ *
+ * 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 threads = getAllVirtualThreads(lookup); + if (threads.isEmpty()) { + return null; + } + + HashMap> 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 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> e : groupedByStackTrace.entrySet()) { + String st = e.getKey(); + ArrayList 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 getAllVirtualThreads(MethodHandles.Lookup lookup) throws Throwable { + if (!init(lookup)) return null; + + ArrayList result = new ArrayList<>(); + for (Object container : getAllContainers()) { + Object /*Stream*/ threads = containerThreadsHandle.invoke(container); + Iterator it = (Iterator)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 getAllContainers() throws Throwable { + ArrayList allContainers = new ArrayList<>(); + Object rootContainer = containersRootHandle.invoke(); + collectContainers(allContainers, rootContainer); + return allContainers; + } + + private static void collectContainers(ArrayList allContainers, Object container) throws Throwable { + allContainers.add(container); + Object/*Stream*/ children = containerChildrenHandle.invoke(container); + Iterator it = (Iterator)streamIteratorHandle.invoke(children); + while (it.hasNext()) { + Object/*ThreadContainer*/ child = it.next(); + collectContainers(allContainers, child); + } + } +} diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index 071cc8ff0a43..6fe122c6d2d6 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -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 diff --git a/plugins/kotlin/plugin/common/resources/META-INF/jvm-debugger.xml b/plugins/kotlin/plugin/common/resources/META-INF/jvm-debugger.xml index e475eaa52b70..4064d89f4722 100644 --- a/plugins/kotlin/plugin/common/resources/META-INF/jvm-debugger.xml +++ b/plugins/kotlin/plugin/common/resources/META-INF/jvm-debugger.xml @@ -34,6 +34,7 @@ +