mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[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:
committed by
intellij-monorepo-bot
parent
5b5bd49445
commit
53e920e5e7
@@ -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"/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user