[debugger] speed up gathering of virtual threads for thread dump, IDEA-367627, IDEA-367848

Evaluate everything in helper, no JDI requests for virtual threads

GitOrigin-RevId: d1f367e9def432554623189f673bd05759ec088b
This commit is contained in:
Vladimir Parfinenko
2025-03-25 21:48:45 +00:00
committed by intellij-monorepo-bot
parent b0c6c0d7bf
commit e3a60db2aa
5 changed files with 174 additions and 96 deletions
@@ -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, ...)"/>
<registryKey key="debugger.thread.dump.include.virtual.threads" defaultValue="true"
description="Try to get virtual threads during debugger's Get Thread Dump action"/>
<registryKey key="debugger.thread.dump.virtual.threads.with.monitors.max.count" defaultValue="1000"
description="Maximum number of virtual threads when debugger still tries to collect information about owned/contended monitors"/>
</extensions>
<actions>
@@ -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<Pair<ThreadReference, String>>,
virtualThreads: List<Triple<ThreadReference, String, Long>>,
): List<ThreadState> {
// 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<ThreadState>()
val nameToThreadMap = mutableMapOf<String, ThreadState>()
val waitingMap = mutableMapOf<String, String>() // key 'waits_for' value
for ((threadReference, rawStackTrace) in allThreads) {
fun processOne(threadReference: ThreadReference, virtualThreadInfo: Pair<String, Long>?) {
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<Int, MutableList<ObjectReference>>()
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<Pair<ThreadReference, String>> {
return vmProxy.virtualMachine.allThreads().asSequence().map { threadReference ->
ProgressManager.checkCanceled()
private fun getStackTrace(threadReference: ThreadReference): String {
val frames =
try {
threadReference.frames()
}
catch (e: IncompatibleThreadStateException) {
logger<ThreadDumpAction>().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<ThreadDumpAction>().error(e)
append("Invalid stack frame: ").append(e.message)
}
}
}
}
private fun splitFirstTwoAndRemainingLines(text: String): Triple<String, String, String> {
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<Pair<ThreadReference, String>> {
private fun evaluateAndGetAllVirtualThreads(suspendContext: SuspendContextImpl): List<Triple<ThreadReference, String, Long>> {
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))
}
}
}
@@ -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(), "<merged>")
stackTrace.substringAfter("\n").replace("<0x\\d+>\\s".toRegex(), "<merged>")
override val item: JavaThreadDumpItem get() = this@JavaThreadDumpItem
@@ -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.
* <br/>
* They are grouped by equal stack traces and packed into the plain `Object` array in the following way:
* <ul>
@@ -56,7 +59,7 @@ public final class VirtualThreadDumper {
* <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.
* 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<String, ArrayList<Thread>> 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:
// <name>
// <javaThreadState>
// <stack trace elements...>
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<String, ArrayList<Thread>> e : groupedByStackTrace.entrySet()) {
String st = e.getKey();
ArrayList<Thread> 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<Thread> getAllVirtualThreads(MethodHandles.Lookup lookup) throws Throwable {
@@ -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);