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);