diff --git a/java/debugger/impl/resources/META-INF/java-debugger.xml b/java/debugger/impl/resources/META-INF/java-debugger.xml
index bd79d1203903..4dc4edf9de91 100644
--- a/java/debugger/impl/resources/META-INF/java-debugger.xml
+++ b/java/debugger/impl/resources/META-INF/java-debugger.xml
@@ -189,6 +189,8 @@
description="Timeout (in ms) for Get Thread Dump action to wait until we suspend the VM to perform extended dump collection (virtual threads, coroutines, ...)"/>
+
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 7494b968e3f3..3f00a8d69cc2 100644
--- a/java/debugger/impl/src/com/intellij/debugger/actions/ThreadDumpAction.kt
+++ b/java/debugger/impl/src/com/intellij/debugger/actions/ThreadDumpAction.kt
@@ -17,6 +17,7 @@ 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.logger
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.progress.ProgressManager
@@ -35,16 +36,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.NonNls
-import java.lang.Long
+import java.lang.Long as JLong
import java.util.concurrent.CancellationException
import kotlin.Int
-import kotlin.Pair
import kotlin.String
import kotlin.Throwable
import kotlin.checkNotNull
import kotlin.let
import kotlin.time.Duration.Companion.milliseconds
-import kotlin.time.Duration.Companion.seconds
import kotlin.to
class ThreadDumpAction : DumbAwareAction() {
@@ -174,7 +173,7 @@ private fun renderObject(monitor: ObjectReference): String {
catch (e: Throwable) {
monitorTypeName = "Error getting object type: '" + e.message + "'"
}
- return "<0x" + Long.toHexString(monitor.uniqueID()) + "> (a " + monitorTypeName + ")"
+ return "<0x" + JLong.toHexString(monitor.uniqueID()) + "> (a " + monitorTypeName + ")"
}
private fun threadStatusToJavaThreadState(status: Int): String {
@@ -203,10 +202,24 @@ private fun threadStatusToState(status: Int): String {
}
}
-private fun threadName(threadReference: ThreadReference): String {
- return threadReference.name() + "@" + threadReference.uniqueID()
+private fun javaThreadStateToState(javaThreadState: String): String {
+ return when (javaThreadState) {
+ Thread.State.BLOCKED.name -> "waiting for monitor entry"
+ Thread.State.NEW.name -> "not started"
+ Thread.State.RUNNABLE.name -> "runnable"
+ Thread.State.TIMED_WAITING.name -> "sleeping"
+ Thread.State.WAITING.name -> "waiting"
+ Thread.State.TERMINATED.name -> "zombie"
+ else -> "undefined"
+ }
}
+private fun threadName(threadReference: ThreadReference): String =
+ threadName(threadReference.name(), threadReference)
+
+private fun threadName(threadNameRaw: String, threadReference: ObjectReference): String =
+ threadNameRaw + "@" + threadReference.uniqueID()
+
private fun getThreadField(
fieldName: String,
threadType: ReferenceType, threadObj: ThreadReference,
@@ -228,82 +241,98 @@ private fun getThreadField(
private fun buildThreadStates(
vmProxy: VirtualMachineProxyImpl,
- virtualThreads: List>,
+ virtualThreads: List>,
): List {
- // By default it includes only platform threads. Unless JDWP's option includevirtualthreads is enabled.
- // TODO: remove duplicates if includevirtualthreads is enabled.
- val platformThreads = getPlatformThreadsWithStackTraces(vmProxy)
-
- val allThreads = platformThreads + virtualThreads.asSequence()
-
val result = mutableListOf()
val nameToThreadMap = mutableMapOf()
val waitingMap = mutableMapOf() // key 'waits_for' value
- for ((threadReference, rawStackTrace) in allThreads) {
+
+ fun processOne(threadReference: ThreadReference, virtualThreadInfo: Pair?) {
ProgressManager.checkCanceled()
- val buffer = StringBuilder()
- val threadStatus = threadReference.status()
- if (threadStatus == ThreadReference.THREAD_STATUS_ZOMBIE) {
- continue
+ val threadName: String
+ val stateString: String
+ val javaThreadStateString: String
+ val isVirtual: Boolean
+ val isDaemon: Boolean
+ val tid: Long?
+ val prio: Int?
+ val rawStackTrace: String
+ if (virtualThreadInfo != null) {
+ val nameStateAndStackTrace = splitFirstTwoAndRemainingLines(virtualThreadInfo.first)
+ val nameRaw = nameStateAndStackTrace.first
+ javaThreadStateString = nameStateAndStackTrace.second
+ rawStackTrace = nameStateAndStackTrace.third
+
+ if (javaThreadStateString == Thread.State.TERMINATED.name) return
+
+ threadName = threadName(nameRaw, threadReference)
+ stateString = javaThreadStateToState(javaThreadStateString)
+
+ tid = virtualThreadInfo.second
+
+ isVirtual = true
+ isDaemon = false
+ prio = null
}
- val threadName = threadName(threadReference)
- val threadState = ThreadState(threadName, threadStatusToState(threadStatus))
- nameToThreadMap[threadName] = threadState
- result += threadState
- threadState.javaThreadState = threadStatusToJavaThreadState(threadStatus)
+ else {
+ val threadStatus = threadReference.status()
+ if (threadStatus == ThreadReference.THREAD_STATUS_ZOMBIE) return
- buffer.append("\"").append(threadName).append("\"")
+ threadName = threadName(threadReference)
+ stateString = threadStatusToState(threadStatus)
+ javaThreadStateString = threadStatusToJavaThreadState(threadStatus)
+
+ isVirtual = threadReference is ThreadReferenceImpl && threadReference.isVirtual
+
+ rawStackTrace = getStackTrace(threadReference)
- val threadType = threadReference.referenceType()
- if (threadType != null) {
// Since Project Loom some of Thread's fields are encapsulated into FieldHolder,
// so we try to look up fields in the thread itself and in its holder.
+ val threadType = threadReference.referenceType()
val (holderObj, holderType) = when (val value = getThreadField("holder", threadType, threadReference, null, null)) {
is ObjectReference -> value to value.referenceType()
else -> null to null
}
-
- when (val value = getThreadField("daemon", threadType, threadReference, holderType, holderObj)) {
- is BooleanValue ->
- if (value.booleanValue()) {
- buffer.append(" daemon")
- threadState.isDaemon = true
- }
- }
-
- when (val value = getThreadField("priority", threadType, threadReference, holderType, holderObj)) {
- is IntegerValue ->
- buffer.append(" prio=").append(value.intValue())
- }
-
- when (val value = getThreadField("tid", threadType, threadReference, holderType, holderObj)) {
- is LongValue -> {
- buffer.append(" tid=0x").append(Long.toHexString(value.longValue()))
- buffer.append(" nid=NA")
- }
- }
+ isDaemon = (getThreadField("daemon", threadType, threadReference, holderType, holderObj) as BooleanValue?)?.booleanValue() ?: false
+ prio = (getThreadField("priority", threadType, threadReference, holderType, holderObj) as IntegerValue?)?.intValue()
+ tid = (getThreadField("tid", threadType, threadReference, holderType, holderObj) as LongValue?)?.longValue()
}
- if (threadReference is ThreadReferenceImpl && threadReference.isVirtual()) {
+ val threadState = ThreadState(threadName, stateString)
+ threadState.javaThreadState = javaThreadStateString
+ nameToThreadMap[threadName] = threadState
+ result += threadState
+
+ val buffer = StringBuilder()
+ buffer.append('"').append(threadName).append('"')
+
+ if (isDaemon) {
+ buffer.append(" daemon")
+ threadState.isDaemon = true
+ }
+ if (prio != null) {
+ buffer.append(" prio=").append(prio)
+ }
+ if (tid != null) {
+ buffer.append(" tid=0x").append(JLong.toHexString(tid))
+ buffer.append(" nid=NA")
+ }
+ if (isVirtual) {
buffer.append(" virtual")
threadState.isVirtual = true
}
- //ThreadGroupReference groupReference = threadReference.threadGroup();
- //if (groupReference != null) {
- // buffer.append(", ").append(JavaDebuggerBundle.message("threads.export.attribute.label.group", groupReference.name()));
- //}
- val state = threadState.state
- if (state != null) {
- buffer.append(" ").append(state)
- }
+ buffer.append(" ").append(threadState.state)
buffer.append("\n java.lang.Thread.State: ").append(threadState.javaThreadState)
+ // There could be too many virtual threads and it's too expensive to collect locking information for all of them.
+ val collectMonitorsInfo = virtualThreadInfo == null ||
+ virtualThreads.size < Registry.intValue("debugger.thread.dump.virtual.threads.with.monitors.max.count", 1000)
try {
- if (vmProxy.canGetOwnedMonitorInfo() && vmProxy.canGetMonitorInfo()) {
+ if (collectMonitorsInfo && vmProxy.canGetOwnedMonitorInfo() && vmProxy.canGetMonitorInfo()) {
val list = threadReference.ownedMonitors()
for (reference in list) {
if (!vmProxy.canGetMonitorFrameInfo()) { // java 5 and earlier
@@ -318,7 +347,7 @@ private fun buildThreadStates(
}
}
- val waitedMonitor = if (vmProxy.canGetCurrentContendedMonitor()) threadReference.currentContendedMonitor() else null
+ val waitedMonitor = if (collectMonitorsInfo && vmProxy.canGetCurrentContendedMonitor()) threadReference.currentContendedMonitor() else null
if (waitedMonitor != null) {
if (vmProxy.canGetMonitorInfo()) {
val waitedMonitorOwner = waitedMonitor.owningThread()
@@ -332,7 +361,7 @@ private fun buildThreadStates(
}
val lockedAt = mutableMapOf>()
- if (vmProxy.canGetMonitorFrameInfo()) {
+ if (collectMonitorsInfo && vmProxy.canGetMonitorFrameInfo()) {
for (m in threadReference.ownedMonitorsAndFrames()) {
if (m is MonitorInfo) { // see JRE-937
val monitors = lockedAt.getOrPut(m.stackDepth()) { mutableListOf() }
@@ -364,11 +393,26 @@ private fun buildThreadStates(
catch (_: IncompatibleThreadStateException) {
buffer.append("\n\t Incompatible thread state: thread not suspended")
}
+
val hasEmptyStack = rawStackTrace.isEmpty()
threadState.setStackTrace(buffer.toString(), hasEmptyStack)
ThreadDumpParser.inferThreadStateDetail(threadState)
}
+ // By default, it includes only platform threads. Unless JDWP's option includevirtualthreads is enabled.
+ val threadsFromVM = vmProxy.virtualMachine.allThreads()
+ threadsFromVM.forEach {
+ processOne(it, null)
+ }
+
+ val threadsFromVMSet = threadsFromVM.toSet()
+ virtualThreads.forEach { (vthread, stackTrace, tid) ->
+ // thread might be already processed if JDWP's option includevirtualthreads is enabled.
+ if (vthread !in threadsFromVMSet) {
+ processOne(vthread, stackTrace to tid)
+ }
+ }
+
for ((waiting, awaited) in waitingMap) {
val waitingThread = nameToThreadMap[waiting] ?: continue // continue if zombie
val awaitedThread = nameToThreadMap[awaited] ?: continue // continue if zombie
@@ -390,35 +434,40 @@ private fun buildThreadStates(
return result
}
-private fun getPlatformThreadsWithStackTraces(vmProxy: VirtualMachineProxyImpl): Sequence> {
- return vmProxy.virtualMachine.allThreads().asSequence().map { threadReference ->
- ProgressManager.checkCanceled()
+private fun getStackTrace(threadReference: ThreadReference): String {
+ val frames =
+ try {
+ threadReference.frames()
+ }
+ catch (e: IncompatibleThreadStateException) {
+ logger().error(e)
+ return "Incompatible thread state: thread not suspended"
+ }
- val frames =
+ return buildString {
+ for (stackFrame in frames) {
+ if (this.isNotEmpty()) {
+ append('\n')
+ }
+ append("\t")
try {
- threadReference.frames()
+ append(ThreadDumpAction.renderLocation(stackFrame.location()))
}
- catch (_: IncompatibleThreadStateException) {
- return@map threadReference to "Incompatible thread state: thread not suspended"
- }
-
- threadReference to buildString {
- for (stackFrame in frames) {
- if (this.isNotEmpty()) {
- append('\n')
- }
- append("\t")
- try {
- append(ThreadDumpAction.renderLocation(stackFrame.location()))
- }
- catch (e: InvalidStackFrameException) {
- append("Invalid stack frame: ").append(e.message)
- }
+ catch (e: InvalidStackFrameException) {
+ logger().error(e)
+ append("Invalid stack frame: ").append(e.message)
}
}
}
}
+private fun splitFirstTwoAndRemainingLines(text: String): Triple {
+ val first = text.lineSequence().first()
+ val second = text.lineSequence().drop(1).first()
+ val remaining = text.lineSequence().drop(2).joinToString("\n")
+ return Triple(first, second, remaining)
+}
+
private class JavaThreadsProvider : ThreadDumpItemsProviderFactory() {
override fun getProvider(context: DebuggerContextImpl) = object : ThreadDumpItemsProvider {
val vm = context.debugProcess!!.virtualMachineProxy
@@ -441,7 +490,7 @@ private class JavaThreadsProvider : ThreadDumpItemsProviderFactory() {
.map(::JavaThreadDumpItem)
}
- private fun evaluateAndGetAllVirtualThreads(suspendContext: SuspendContextImpl): List> {
+ private fun evaluateAndGetAllVirtualThreads(suspendContext: SuspendContextImpl): List> {
val evaluationContext = EvaluationContextImpl(suspendContext, suspendContext.frameProxy)
val lookupImpl = getMethodHandlesImplLookup(evaluationContext)
@@ -461,19 +510,23 @@ private class JavaThreadsProvider : ThreadDumpItemsProviderFactory() {
thisLogger().error(e)
return emptyList()
}
- val packedThreadsAndStackTraces = (evaluated as ArrayReference?)?.values ?: emptyList()
+ if (evaluated == null) return emptyList()
+
+ val (packedThreadsAndStackTraces, threadIds) = (evaluated as ArrayReference).values.map { (it as ArrayReference).values }
ProgressManager.checkCanceled()
return buildList {
- var i = 0
- while (i < packedThreadsAndStackTraces.size) {
- val stackTrace = (packedThreadsAndStackTraces[i++] as StringReference).value()
+ var tidIdx = 0
+ var stIdx = 0
+ while (stIdx < packedThreadsAndStackTraces.size) {
+ val stackTrace = (packedThreadsAndStackTraces[stIdx++] as StringReference).value()
while (true) {
- val thread = packedThreadsAndStackTraces[i++]
+ val thread = packedThreadsAndStackTraces[stIdx++]
if (thread == null) {
break
}
- add(thread as ThreadReference to stackTrace)
+ val threadId = (threadIds[tidIdx++] as LongValue).value()
+ add(Triple(thread as ThreadReference, stackTrace, threadId))
}
}
}
diff --git a/java/java-impl/src/com/intellij/unscramble/DumpItem.kt b/java/java-impl/src/com/intellij/unscramble/DumpItem.kt
index c4cc0c89931e..920b74fdd03e 100644
--- a/java/java-impl/src/com/intellij/unscramble/DumpItem.kt
+++ b/java/java-impl/src/com/intellij/unscramble/DumpItem.kt
@@ -174,7 +174,7 @@ class JavaThreadDumpItem(private val threadState: ThreadState) : DumpItem {
private inner class JavaMergeableToken : MergeableToken {
private val comparableStackTrace: String =
- stackTrace.substringAfter("\n").replace("<0x.+>\\s".toRegex(), "")
+ stackTrace.substringAfter("\n").replace("<0x\\d+>\\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
index 0bde38599bd8..fc0a20cbcd96 100644
--- a/java/java-runtime/src/com/intellij/rt/debugger/VirtualThreadDumper.java
+++ b/java/java-runtime/src/com/intellij/rt/debugger/VirtualThreadDumper.java
@@ -20,6 +20,7 @@ public final class VirtualThreadDumper {
static MethodHandle containerThreadsHandle;
static MethodHandle threadIsVirtualHandle;
+ static MethodHandle threadThreadState;
private static boolean init(MethodHandles.Lookup lookup) {
if (!initialized) {
@@ -35,6 +36,8 @@ public final class VirtualThreadDumper {
//noinspection JavaLangInvokeHandleSignature
threadIsVirtualHandle = lookup.findVirtual(Thread.class, "isVirtual", MethodType.methodType(boolean.class));
+ //noinspection JavaLangInvokeHandleSignature
+ threadThreadState = lookup.findVirtual(Thread.class, "threadState", MethodType.methodType(Thread.State.class));
successfully = true;
} catch (NoSuchMethodException | IllegalAccessException | ClassNotFoundException e) {
@@ -46,7 +49,7 @@ public final class VirtualThreadDumper {
}
/**
- * Returns all virtual threads with stack traces.
+ * Returns all virtual threads with stack traces (along with a name and thread state) and parallel array of thread IDs.
*
* They are grouped by equal stack traces and packed into the plain `Object` array in the following way:
*
@@ -56,7 +59,7 @@ public final class VirtualThreadDumper {
* - 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.
+ * Returns {@code null} if there are no virtual threads or some error occurred.
*/
public static Object[] getAllVirtualThreadsWithStackTraces(MethodHandles.Lookup lookup) throws Throwable {
if (!init(lookup)) {
@@ -71,8 +74,18 @@ public final class VirtualThreadDumper {
HashMap> groupedByStackTrace = new HashMap<>();
for (Thread t : threads) {
StringBuilder buffer = new StringBuilder();
+
+ // "Stack trace" format, in such a way it should be shared between multiple threads and easily processed on the debugger side:
+ //
+ //
+ //
+
+ String name = t.getName();
+ Thread.State javaThreadState = (Thread.State)threadThreadState.invoke(t);
+ buffer.append(name).append('\n').append(javaThreadState);
+
for (StackTraceElement ste : t.getStackTrace()) {
- buffer.append("\tat ").append(ste).append('\n');
+ buffer.append("\n\tat ").append(ste);
}
String stackTrace = buffer.toString();
@@ -84,21 +97,25 @@ public final class VirtualThreadDumper {
similarThreads.add(t);
}
+ long[] tids = new long[threads.size()];
+ int tidIdx = 0;
+
Object[] allStackTraceAndThreads = new Object[threads.size() + groupedByStackTrace.size() * 2];
- int i = 0;
+ int stIdx = 0;
for (Map.Entry> e : groupedByStackTrace.entrySet()) {
String st = e.getKey();
ArrayList ts = e.getValue();
- allStackTraceAndThreads[i++] = st;
+ allStackTraceAndThreads[stIdx++] = st;
for (Thread t : ts) {
- allStackTraceAndThreads[i++] = t;
+ allStackTraceAndThreads[stIdx++] = t;
+ tids[tidIdx++] = t.getId();
}
- allStackTraceAndThreads[i++] = null;
+ allStackTraceAndThreads[stIdx++] = null;
}
- assert i == allStackTraceAndThreads.length;
+ assert stIdx == allStackTraceAndThreads.length;
- return allStackTraceAndThreads;
+ return new Object[] { allStackTraceAndThreads, tids };
}
private static ArrayList getAllVirtualThreads(MethodHandles.Lookup lookup) throws Throwable {
diff --git a/platform/threadDumpParser/src/com/intellij/threadDumpParser/ThreadDumpParser.java b/platform/threadDumpParser/src/com/intellij/threadDumpParser/ThreadDumpParser.java
index fb381bd9e88f..e62dcb88f6db 100644
--- a/platform/threadDumpParser/src/com/intellij/threadDumpParser/ThreadDumpParser.java
+++ b/platform/threadDumpParser/src/com/intellij/threadDumpParser/ThreadDumpParser.java
@@ -34,6 +34,7 @@ public final class ThreadDumpParser {
private static final Pattern ourIdleTimerThreadPattern = Pattern.compile("java\\.lang\\.Object\\.wait\\([^()]+\\)\\s+at java\\.util\\.TimerThread\\.mainLoop");
private static final Pattern ourIdleSwingTimerThreadPattern = Pattern.compile("java\\.lang\\.Object\\.wait\\([^()]+\\)\\s+at javax\\.swing\\.TimerQueue\\.run");
private static final String AT_JAVA_LANG_OBJECT_WAIT = "java.lang.Object.wait(";
+ private static final String ourLockedOwnableSynchronizersHeader = "Locked ownable synchronizers";
private static final Pattern ourLockedOwnableSynchronizersPattern = Pattern.compile("- <(0x[\\da-f]+)> \\(.*\\)");
private static final String[] IMPORTANT_THREAD_DUMP_WORDS = ContainerUtil.ar("tid", "nid", "wait", "parking", "prio", "os_prio", "java");
@@ -217,6 +218,11 @@ public final class ThreadDumpParser {
}
private static @Nullable String findLockedOwnableSynchronizers(final String stackTrace) {
+ if (!stackTrace.contains(ourLockedOwnableSynchronizersHeader)) {
+ // It's a fast path, otherwise regex below takes too much time.
+ return null;
+ }
+
Matcher m = ourLockedOwnableSynchronizersPattern.matcher(stackTrace);
if (m.find()) {
return m.group(1);