From 0cc3449b3aa21b349c52c731eafcb46bdc654228 Mon Sep 17 00:00:00 2001 From: Maria Sokolova Date: Wed, 28 Jan 2026 13:15:46 +0100 Subject: [PATCH] IDEA-384931 [debugger]: Collect the hierarchy of thread containers for virtual threads GitOrigin-RevId: ea0c34a1f0f0718e0dc534ab738adbbdb308cb17 --- .../backend/BackendJavaDebuggerSessionApi.kt | 6 +- .../debugger/actions/ThreadDumpAction.kt | 60 +++-- .../impl/shared/actions/ThreadDumpAction.kt | 6 + .../impl/shared/rpc/JavaDebuggerSessionApi.kt | 3 + .../src/com/intellij/unscramble/DumpItem.kt | 105 ++++++++- .../rt/debugger/VirtualThreadDumper.java | 215 ++++++++++-------- .../unscramble/ThreadDumpPanelTest.kt | 160 ------------- .../threadDumpParser/ThreadState.java | 10 + .../view/CoroutinesDumpAsyncProvider.kt | 8 + 9 files changed, 287 insertions(+), 286 deletions(-) delete mode 100644 java/java-tests/testSrc/com/intellij/unscramble/ThreadDumpPanelTest.kt diff --git a/java/debugger/backend/src/com/intellij/java/debugger/impl/backend/BackendJavaDebuggerSessionApi.kt b/java/debugger/backend/src/com/intellij/java/debugger/impl/backend/BackendJavaDebuggerSessionApi.kt index d0e4f8638ddb..ab0ec8206e17 100644 --- a/java/debugger/backend/src/com/intellij/java/debugger/impl/backend/BackendJavaDebuggerSessionApi.kt +++ b/java/debugger/backend/src/com/intellij/java/debugger/impl/backend/BackendJavaDebuggerSessionApi.kt @@ -217,7 +217,11 @@ private fun dumpItemDtos(allDumpItems: List, maxItems: Int): ThreadDum isDeadLocked = it.isDeadLocked, stackTraceIndex = stackTraceIndex, iconToolTipIndex = iconToolTipToIndex[it.iconToolTip]!!.toByte(), - firstLine = firstLine) + firstLine = firstLine, + isContainer = it.isContainer, + id = it.id, + parentId = it.parentId, + ) } return ThreadDumpWithAwaitingDependencies(items = items, 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 bef97ee1dcc2..a0152b757c50 100644 --- a/java/debugger/impl/src/com/intellij/debugger/actions/ThreadDumpAction.kt +++ b/java/debugger/impl/src/com/intellij/debugger/actions/ThreadDumpAction.kt @@ -73,7 +73,7 @@ class ThreadDumpAction { @JvmStatic fun buildThreadStates(vmProxy: VirtualMachineProxyImpl): List { val platformThreads = vmProxy.virtualMachine.allThreads() - return buildThreadStates(vmProxy, platformThreads, virtualThreads = emptyList()) + return buildThreadStates(vmProxy, platformThreads, virtualThreads = emptyList(), emptyList()) } @JvmStatic @@ -85,7 +85,7 @@ class ThreadDumpAction { @ApiStatus.Internal suspend fun buildThreadDump(context: DebuggerContextImpl, onlyPlatformThreads: Boolean, dumpItemsChannel: SendChannel>) { suspend fun sendJavaPlatformThreads() { - val platformThreads = buildJavaPlatformThreadDump(context).toDumpItems() + val platformThreads = toDumpItems(buildJavaPlatformThreadDump()) dumpItemsChannel.send(platformThreads) } @@ -185,7 +185,7 @@ class ThreadDumpAction { } } - fun buildJavaPlatformThreadDump(context: DebuggerContextImpl): List { + fun buildJavaPlatformThreadDump(): List { val vm = VirtualMachineProxyImpl.getCurrent() vm.suspend() try { @@ -315,6 +315,7 @@ private fun buildThreadStates( vmProxy: VirtualMachineProxyImpl, platformThreads: List, virtualThreads: List>, + threadContainerRefs: List ): List { val result = mutableListOf() @@ -355,21 +356,23 @@ private fun buildThreadStates( val threadName: String val stateString: String val javaThreadStateString: String + val threadContainerId: Long? 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 + val lines = virtualThreadInfo.first.lineSequence() + val (nameRaw, javaThreadState, threadContainerIdx) = lines.take(3).toList() + rawStackTrace = lines.drop(3).joinToString("\n") - if (javaThreadStateString == Thread.State.TERMINATED.name) return + if (javaThreadState == Thread.State.TERMINATED.name) return threadName = threadName(nameRaw, threadReference) - stateString = javaThreadStateToState(javaThreadStateString) + stateString = javaThreadStateToState(javaThreadState) + javaThreadStateString = javaThreadState + threadContainerId = threadContainerRefs[threadContainerIdx.toInt()].uniqueID() tid = virtualThreadInfo.second @@ -384,6 +387,7 @@ private fun buildThreadStates( threadName = threadName(threadReference) stateString = threadStatusToState(threadStatus) javaThreadStateString = threadStatusToJavaThreadState(threadStatus) + threadContainerId = null // thread container is not provided for platform threads for now isVirtual = threadReference is ThreadReferenceImpl && threadReference.isVirtual @@ -394,9 +398,11 @@ private fun buildThreadStates( prio = getFieldValue(priorityField, threadReference, holderObj)?.let { (it as IntegerValue).intValue() } tid = getFieldValue(tidField, threadReference, holderObj)?.let { (it as LongValue).longValue() } } - val threadState = ThreadState(threadName, stateString) threadState.javaThreadState = javaThreadStateString + threadState.uniqueId = threadReference.uniqueID() + threadState.threadContainerUniqueId = threadContainerId + nameToThreadMap[threadName] = threadState result += threadState @@ -503,6 +509,9 @@ private fun buildThreadStates( processOne(pthread, virtualThreadInfo = null) } + if (virtualThreads.isNotEmpty()) { + require(threadContainerRefs.isNotEmpty()) { "The list of thread container references was not provided for virtual threads." } + } virtualThreads.forEach { (vthread, stackTrace, tid) -> processOne(vthread, stackTrace to tid) } @@ -554,13 +563,6 @@ private fun getStackTrace(threadReference: ThreadReference): String { } } -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) -} - internal class JavaVirtualThreadsProvider : ThreadDumpItemsProviderFactory() { override fun getProvider(context: DebuggerContextImpl) = object : ThreadDumpItemsProvider { val vm = VirtualMachineProxyImpl.getCurrent() @@ -581,13 +583,12 @@ internal class JavaVirtualThreadsProvider : ThreadDumpItemsProviderFactory() { return ( if (!enabled) emptyList() else { - val virtualThreads = evaluateAndGetAllVirtualThreads(suspendContext!!) - buildThreadStates(vm, platformThreads = emptyList(), virtualThreads).toDumpItems() + evaluateAndGetAllVirtualThreadsDumpItems(suspendContext!!) }) .also { DebuggerStatistics.logVirtualThreadsDump(context.project, it.size) } } - private fun evaluateAndGetAllVirtualThreads(suspendContext: SuspendContextImpl): List> { + private fun evaluateAndGetAllVirtualThreadsDumpItems(suspendContext: SuspendContextImpl): List { val evaluationContext = EvaluationContextImpl(suspendContext, suspendContext.frameProxy) val lookupImpl = getMethodHandlesImplLookup(evaluationContext) @@ -599,7 +600,7 @@ internal class JavaVirtualThreadsProvider : ThreadDumpItemsProviderFactory() { val evaluated = try { DebuggerUtilsImpl.invokeHelperMethod( evaluationContext, - VirtualThreadDumper::class.java, "getAllVirtualThreadsWithStackTraces", + VirtualThreadDumper::class.java, "getAllVirtualThreadsWithStackTracesAndContainers", listOf(lookupImpl) ) } @@ -609,10 +610,22 @@ internal class JavaVirtualThreadsProvider : ThreadDumpItemsProviderFactory() { } if (evaluated == null) return emptyList() - val (packedThreadsAndStackTraces, threadIds) = (evaluated as ArrayReference).values.map { (it as ArrayReference).values } + val packedThreadsAndStackTraces = ((evaluated as ArrayReference).values[0] as ArrayReference).values + val threadIds = (evaluated.values[1] as ArrayReference).values + val threadContainerNames = (evaluated.values[2] as ArrayReference).values.map { (it as StringReference).value() } + val threadContainerRefs = (evaluated.values[3] as ArrayReference).values.map { it as ObjectReference } + val parentContainerOrdinals = (evaluated.values[4] as ArrayReference).values.map { (it as IntegerValue).intValue() } + require(threadContainerNames.size == threadContainerRefs.size) { "The number of thread container names should be equal the number of thread container references." } + require(threadContainerNames.size == parentContainerOrdinals.size) { "The number of thread container names should be equal the number of corresponding parent container ordinals." } + + val threadStates = buildVirtualThreadStates(packedThreadsAndStackTraces, threadIds, threadContainerRefs) + return toDumpItems(threadStates, threadContainerNames, threadContainerRefs, parentContainerOrdinals) + } + + private fun buildVirtualThreadStates(packedThreadsAndStackTraces: List, threadIds: List, threadContainerRefs: List): List { ProgressManager.checkCanceled() - return buildList { + val virtualThreads = buildList { var tidIdx = 0 var stIdx = 0 while (stIdx < packedThreadsAndStackTraces.size) { @@ -627,6 +640,7 @@ internal class JavaVirtualThreadsProvider : ThreadDumpItemsProviderFactory() { } } } + return buildThreadStates(vm, platformThreads = emptyList(), virtualThreads, threadContainerRefs) } } } \ No newline at end of file diff --git a/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/actions/ThreadDumpAction.kt b/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/actions/ThreadDumpAction.kt index 696469489e70..fa1dd2cbfdd9 100644 --- a/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/actions/ThreadDumpAction.kt +++ b/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/actions/ThreadDumpAction.kt @@ -131,8 +131,14 @@ private class FrontendDumpItem( override val attributes: SimpleTextAttributes get() = attributesCache[itemDto.attributesIndex.toInt().toUInt().toInt()] override val isDeadLocked: Boolean get() = itemDto.isDeadLocked override val awaitingDumpItems: Set get() = internalAwaitingItems + override val isContainer: Boolean get() = itemDto.isContainer + override val id: Long get() = itemDto.id + override val parentId: Long? get() = itemDto.parentId fun setAwaitingItems(items: Set) { internalAwaitingItems = items } + + override fun toString(): String = + "FrontendDumpItem(name=$name)" } diff --git a/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/rpc/JavaDebuggerSessionApi.kt b/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/rpc/JavaDebuggerSessionApi.kt index 0c31a65cc01e..20a8d1f0f43d 100644 --- a/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/rpc/JavaDebuggerSessionApi.kt +++ b/java/debugger/shared/src/com/intellij/java/debugger/impl/shared/rpc/JavaDebuggerSessionApi.kt @@ -87,4 +87,7 @@ data class JavaThreadDumpItemDto( val iconIndex: Byte, val attributesIndex: Byte, val isDeadLocked: Boolean, + val isContainer: Boolean, + val id: Long, + val parentId: Long?, ) diff --git a/java/java-frontback-impl/src/com/intellij/unscramble/DumpItem.kt b/java/java-frontback-impl/src/com/intellij/unscramble/DumpItem.kt index b41658911057..abf8b992a5c7 100644 --- a/java/java-frontback-impl/src/com/intellij/unscramble/DumpItem.kt +++ b/java/java-frontback-impl/src/com/intellij/unscramble/DumpItem.kt @@ -7,6 +7,7 @@ import com.intellij.openapi.util.NlsSafe import com.intellij.threadDumpParser.ThreadOperation import com.intellij.threadDumpParser.ThreadState import com.intellij.ui.SimpleTextAttributes +import com.sun.jdi.ObjectReference import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.Nls import java.awt.Color @@ -37,6 +38,21 @@ interface DumpItem { */ val awaitingDumpItems: Set + /** + * Unique identifier of the dump item. + */ + val id: Long + + /** + * Unique identifier of the parent dump item. + */ + val parentId: Long? + + /** + * True if the given dump item can be a parent to some other dump item. + */ + val isContainer: Boolean + companion object { @JvmField val SLEEPING_ATTRIBUTES: SimpleTextAttributes = SimpleTextAttributes.GRAY_ATTRIBUTES @@ -107,20 +123,46 @@ class CompoundDumpItem( } @ApiStatus.Internal -fun List.toDumpItems(): List { - val statesToItems = associateWith(::JavaThreadDumpItem) +fun toDumpItems(threadStates: List): List = + toDumpItems(threadStates, emptyList(), emptyList(), emptyList()) + +@ApiStatus.Internal +fun toDumpItems(threadStates: List, threadContainerNames: List, threadContainerRefs: List, parentContainerOrdinals: List): List { + val dumpItems = threadStates.map(::JavaThreadDumpItem) + + val statesToItems = threadStates.zip(dumpItems).toMap() for ((threadState, dumpItem) in statesToItems) { val awaitingItems = threadState.awaitingThreads.mapNotNull { statesToItems[it] }.toSet() dumpItem.setAwaitingItems(awaitingItems) } - return statesToItems.values.toList() + val threadContainers = threadContainerNames.withIndex().map { (index, name) -> + val id = threadContainerRefs[index].uniqueID() + val parentOrdinal = parentContainerOrdinals[index] + if (parentOrdinal == -1) { + JavaVirtualThreadContainerItem(name, id, null) + } else { + val parentId = threadContainerRefs[parentOrdinal].uniqueID() + JavaVirtualThreadContainerItem(name, id, parentId) + } + } + + return dumpItems + threadContainers } private class JavaThreadDumpItem(private val threadState: ThreadState) : MergeableDumpItem { override val name: String = threadState.name + override val isContainer: Boolean + get() = false + + override val id: Long + get() = threadState.uniqueId + + override val parentId: Long? + get() = threadState.threadContainerUniqueId + override val stateDesc: String get() { val trimmedState = (threadState.threadStateDetail ?: threadState.state).let { @@ -226,6 +268,7 @@ private class JavaThreadDumpItem(private val threadState: ThreadState) : Mergeab if (threadState.awaitingThreads != otherThreadState.awaitingThreads) return false if (threadState.deadlockedThreads != otherThreadState.deadlockedThreads) return false if (this.comparableStackTrace != other.comparableStackTrace) return false + if (this.item.parentId != other.item.parentId) return false return true } @@ -239,12 +282,59 @@ private class JavaThreadDumpItem(private val threadState: ThreadState) : Mergeab threadState.extraState, threadState.awaitingThreads, threadState.deadlockedThreads, - comparableStackTrace + comparableStackTrace, + parentId ) } } } +private class JavaVirtualThreadContainerItem(private val containerName: String, override val id: Long, override val parentId: Long?) : MergeableDumpItem { + override val name: @NlsSafe String + get() = formatThreadContainerName(containerName) + + override val isContainer: Boolean + get() = true + + override val stateDesc: @NlsSafe String + get() = "" // todo we can add threadsCount to the state + + override val attributes: SimpleTextAttributes + get() = SimpleTextAttributes.REGULAR_ATTRIBUTES + + override val stackTrace: @NlsSafe String + get() = "" + override val interestLevel: Int + get() = 100 // todo dependent on the number of children, for now kept on top + override val icon: Icon + get() = IconsCache.getIconWithVirtualOverlay(AllIcons.Debugger.ThreadGroup) + override val iconToolTip: @Nls String + get() = JavaFrontbackBundle.message("dump.item.java.thread.icon.tooltip.container") + override val isDeadLocked: Boolean + get() = false + override val awaitingDumpItems: Set + get() = emptySet() + + override val mergeableToken: MergeableToken = object : MergeableToken { + override fun equals(other: Any?) = super.equals(other) + override fun hashCode() = super.hashCode() + override val item = this@JavaVirtualThreadContainerItem + } + + companion object { + // see jdk.internal.vm.ThreadContainers.RootContainer.name + const val ROOT = "" + const val VIRTUAL_THREADS_ROOT_CONTAINER = "Root Container of Virtual Threads" + const val JUC_PACKAGE = "java.util.concurrent" + + fun formatThreadContainerName(name: String) = when { + name == ROOT -> VIRTUAL_THREADS_ROOT_CONTAINER + name.startsWith(JUC_PACKAGE) -> name.removePrefix(JUC_PACKAGE) + else -> name + } + } +} + class InfoDumpItem(private val title: @Nls String, private val details: @NlsSafe String) : MergeableDumpItem { override val mergeableToken: MergeableToken = object : MergeableToken { override fun equals(other: Any?) = super.equals(other) @@ -270,6 +360,11 @@ class InfoDumpItem(private val title: @Nls String, private val details: @NlsSafe get() = false override val awaitingDumpItems: Set get() = emptySet() - + override val isContainer: Boolean + get() = false + override val id: Long + get() = this.hashCode().toLong() + override val parentId: Long? + get() = null } 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 d73e1b03bf35..0c1d01376411 100644 --- a/java/java-runtime/src/com/intellij/rt/debugger/VirtualThreadDumper.java +++ b/java/java-runtime/src/com/intellij/rt/debugger/VirtualThreadDumper.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.stream.Stream; @SuppressWarnings("unchecked") @@ -16,8 +17,6 @@ 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; @@ -28,18 +27,16 @@ public final class VirtualThreadDumper { 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)); - + // ThreadContainer & Co., since Java 21 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)); + containerChildrenHandle = lookup.findVirtual(threadContainerClass, "children", MethodType.methodType(Stream.class)); + containerThreadsHandle = lookup.findVirtual(threadContainerClass, "threads", MethodType.methodType(Stream.class)); - //noinspection JavaLangInvokeHandleSignature + // VirtualThread & Co., since Java 21 threadIsVirtualHandle = lookup.findVirtual(Thread.class, "isVirtual", MethodType.methodType(boolean.class)); - //noinspection JavaLangInvokeHandleSignature + // Thread, non-public method threadThreadState = lookup.findVirtual(Thread.class, "threadState", MethodType.methodType(Thread.State.class)); successfully = true; @@ -52,107 +49,131 @@ public final class VirtualThreadDumper { } /** - * 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: - *
    - *
  • 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 {@code null} if there are no virtual threads or some error occurred. + * Returns an Object array containing information about virtual threads with the following elements: + *
    + *
  1. {@code Object[]} - all virtual threads grouped by equal stack traces. + * The elements in this array are packed as follows: + *
      + *
    • {@link String} representing the common stack trace for the group. + * It includes the thread name, thread state, thread container ordinal (in the following array), and the stack frames.
    • + *
    • Then, there are one or many thread objects as {@link com.sun.jdi.ThreadReference ThreadReferences} + * and each of them has the above stack trace.
    • + *
    • After the last thread object, there is a single {@code null} as a delimiter.
    • + *
    • Then we have a new group of stack trace and threads, or the array ends.
    • + *
    + *
  2. + *
  3. {@code long[]} - thread IDs of threads from the first array in the corresponding order.
  4. + *
  5. {@code String[]} - names of all {@code jdk.internal.vm.ThreadContainer}s, they are referenced from the first array by ordinals.
  6. + *
  7. {@code Object[]} - {@code jdk.internal.vm.ThreadContainer} objects in the same order as their names in the array above.
  8. + *
  9. {@code int[]} - ordinals of the parent container for every thread container or -1 if there is no parent.
  10. + *
*/ - public static Object[] getAllVirtualThreadsWithStackTraces(MethodHandles.Lookup lookup) throws Throwable { + public static Object[] getAllVirtualThreadsWithStackTracesAndContainers(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(); - - // "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("\n\tat ").append(ste); - } - String stackTrace = buffer.toString(); - - ArrayList similarThreads = groupedByStackTrace.get(stackTrace); - if (similarThreads == null) { - similarThreads = new ArrayList<>(); - groupedByStackTrace.put(stackTrace, similarThreads); - } - similarThreads.add(t); - } - - long[] tids = new long[threads.size()]; - int tidIdx = 0; - - Object[] allStackTraceAndThreads = new Object[threads.size() + groupedByStackTrace.size() * 2]; - int stIdx = 0; - - for (Map.Entry> e : groupedByStackTrace.entrySet()) { - String st = e.getKey(); - ArrayList ts = e.getValue(); - allStackTraceAndThreads[stIdx++] = st; - for (Thread t : ts) { - allStackTraceAndThreads[stIdx++] = t; - tids[tidIdx++] = t.getId(); - } - allStackTraceAndThreads[stIdx++] = null; - } - assert stIdx == allStackTraceAndThreads.length; - - return new Object[] { allStackTraceAndThreads, tids }; + return new Collector().collect(); } - private static ArrayList getAllVirtualThreads(MethodHandles.Lookup lookup) throws Throwable { - if (!init(lookup)) return null; + private static class Collector { + int threadsCount = 0; + final HashMap> threadsGroupedByStackTrace = new HashMap<>(); - 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); + final ArrayList containerNames = new ArrayList<>(); + final ArrayList containerReferences = new ArrayList<>(); + final ArrayList containerParentOrdinals = new ArrayList<>(); + + /** + * @see VirtualThreadDumper#getAllVirtualThreadsWithStackTracesAndContainers(MethodHandles.Lookup) + */ + Object[] collect() throws Throwable { + // Collect all threads and containers starting from the root container. + processContainer(containersRootHandle.invoke(), -1); + + // Group threads by stack trace and compact them into arrays. + long[] threadIds = new long[threadsCount]; + int tidIdx = 0; + Object[] allStackTraceAndThreads = new Object[threadsCount + threadsGroupedByStackTrace.size() * 2]; + int stIdx = 0; + for (Map.Entry> e : threadsGroupedByStackTrace.entrySet()) { + String st = e.getKey(); + ArrayList ts = e.getValue(); + allStackTraceAndThreads[stIdx++] = st; + for (Thread t : ts) { + allStackTraceAndThreads[stIdx++] = t; + threadIds[tidIdx++] = t.getId(); } + allStackTraceAndThreads[stIdx++] = null; + } + assert tidIdx == threadsCount; + assert stIdx == allStackTraceAndThreads.length; + + return new Object[] { + allStackTraceAndThreads, + threadIds, + containerNames.toArray(), + containerReferences.toArray(), + containerParentOrdinals.stream().mapToInt(Integer::intValue).toArray() + }; + } + + private void processContainer(Object container, int parentContainerOrdinal) throws Throwable { + int containerOrdinal = saveContainerInfo(container, parentContainerOrdinal); + saveVirtualThreadsInfo(container, containerOrdinal); + processContainerChildren(container, containerOrdinal); + } + + private void saveVirtualThreadsInfo(Object container, int containerOrdinal) throws Throwable { + Iterator threads = ((Stream)containerThreadsHandle.invoke(container)).iterator(); + while (threads.hasNext()) { + Thread t = threads.next(); + + boolean isVirtual = (boolean)threadIsVirtualHandle.invoke(t); + if (!isVirtual) continue; + + String name = t.getName(); + Thread.State javaThreadState = (Thread.State)threadThreadState.invoke(t); + + // "Stack trace" format, in such a way it should be shared between multiple threads and easily processed on the debugger side: + // + // + // + // + StringBuilder buffer = new StringBuilder(); + buffer.append(name).append('\n') + .append(javaThreadState).append('\n') + .append(containerOrdinal); + for (StackTraceElement ste : t.getStackTrace()) { + buffer.append("\n\tat ").append(ste); + } + String stackTrace = buffer.toString(); + + ArrayList similarThreads = threadsGroupedByStackTrace.get(stackTrace); + if (similarThreads == null) { + similarThreads = new ArrayList<>(); + threadsGroupedByStackTrace.put(stackTrace, similarThreads); + } + similarThreads.add(t); + threadsCount++; } } - return result; - } - private static ArrayList getAllContainers() throws Throwable { - ArrayList allContainers = new ArrayList<>(); - Object rootContainer = containersRootHandle.invoke(); - collectContainers(allContainers, rootContainer); - return allContainers; - } + private int saveContainerInfo(Object container, int parentContainerOrdinal) throws Throwable { + assert containerNames.size() == containerParentOrdinals.size(); + int ordinal = containerNames.size(); + containerNames.add(container.toString()); + containerReferences.add(container); + containerParentOrdinals.add(parentContainerOrdinal); + return ordinal; + } - 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); + private void processContainerChildren(Object container, int ordinal) throws Throwable { + Iterator children = ((Stream)containerChildrenHandle.invoke(container)).iterator(); + while (children.hasNext()) { + Object childContainer = children.next(); + processContainer(childContainer, ordinal); + } } } } diff --git a/java/java-tests/testSrc/com/intellij/unscramble/ThreadDumpPanelTest.kt b/java/java-tests/testSrc/com/intellij/unscramble/ThreadDumpPanelTest.kt deleted file mode 100644 index 4e43bcc426b6..000000000000 --- a/java/java-tests/testSrc/com/intellij/unscramble/ThreadDumpPanelTest.kt +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package com.intellij.unscramble - -import com.intellij.execution.filters.TextConsoleBuilderFactory -import com.intellij.execution.impl.ConsoleViewImpl -import com.intellij.execution.ui.ConsoleView -import com.intellij.icons.AllIcons -import com.intellij.ide.ui.UISettings -import com.intellij.openapi.actionSystem.DefaultActionGroup -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.NlsSafe -import com.intellij.testFramework.LightPlatformTestCase -import com.intellij.ui.SimpleTextAttributes -import com.intellij.ui.treeStructure.Tree -import junit.framework.TestCase -import org.jetbrains.annotations.Nls -import java.util.Objects -import javax.swing.Icon - - -class ThreadDumpPanelTest : LightPlatformTestCase() { - private lateinit var threadDumpPanel: ThreadDumpPanel - private lateinit var myConsoleView: ConsoleView - - @Throws(Exception::class) - override fun setUp() { - super.setUp() - val consoleBuilder = TextConsoleBuilderFactory.getInstance().createBuilder(project) - myConsoleView = consoleBuilder.getConsole() - threadDumpPanel = ThreadDumpPanel.createFromDumpItems(project, myConsoleView, DefaultActionGroup(), emptyList()) - } - - @Throws(Exception::class) - override fun tearDown() { - try { - Disposer.dispose(myConsoleView) - } - catch (e: Throwable) { - addSuppressedException(e) - } - finally { - super.tearDown() - } - } - - fun testBasicDump() { - val tree: Tree = threadDumpPanel.tree - val dumpItems = createBasicDump() - UISettings.getInstance().getState().mergeEqualStackTraces = false - threadDumpPanel.addDumpItems(dumpItems, 0, emptyList(), 0) - TestCase.assertEquals("Should show all unmerged items", 6, tree.model.getChildCount(tree.model.root)) - // Select first item - tree.setSelectionRow(0) - (myConsoleView as ConsoleViewImpl).waitAllRequests() - - // Verify stack trace is printed to console - val document = (myConsoleView as ConsoleViewImpl).editor!!.document - val consoleText = document.text - assertTrue("Console should contain stack trace of the 1st item MyCoroutine1", consoleText.contains("at MainKt.foo(Main.kt:161)")) - - tree.setSelectionRow(2) - (myConsoleView as ConsoleViewImpl).waitAllRequests() - - assertTrue("Console should contain stack trace of the 3rs item Thread3", consoleText.contains("boo(Main.kt:1)")) - } - - private fun createBasicDump(): List { - return listOf( - TestDumpItem( - name = "MyCoroutine1", - stateDesc = "RUNNING on thread Thread1 [BlockingEventLoop@3e53c781]", - stackTrace = "at MainKt.foo(Main.kt:161)\n" + - "\tat MainKt\$main\$1\$t1\$1\$1\$1\$1\$1\$1.invokeSuspend(Main.kt:101)\n" + - "\tat MainKt\$main\$1\$t1\$1\$1\$1\$1\$1.invokeSuspend(Main.kt:100)\n" - ), - TestDumpItem( - name = "MyCoroutine2", - stateDesc = "RUNNING on thread Thread2 [BlockingEventLoop@3e53c781]", - stackTrace = "at MainKt.foo(Main.kt:161)\n" + - "\tat MainKt\$main\$1\$t1\$1\$1\$1\$1\$1\$1.invokeSuspend(Main.kt:101)\n" + - "\tat MainKt\$main\$1\$t1\$1\$1\$1\$1\$1.invokeSuspend(Main.kt:100)\n" - ), - TestDumpItem( - name = "MyCoroutine3", - stateDesc = "RUNNING on thread Thread3 [BlockingEventLoop@3e53c781]", - stackTrace = "at MainKt.foo(Main.kt:161)\n" + - "\tat MainKt\$main\$1\$t1\$1\$1\$1\$1\$1\$1.invokeSuspend(Main.kt:101)\n" + - "\tat MainKt\$main\$1\$t1\$1\$1\$1\$1\$1.invokeSuspend(Main.kt:100)\n" - ), - TestDumpItem( - name = "Thread1", - stateDesc = "\"Thread1\" daemon prio=5 tid=0x24 nid=NA runnable", - stackTrace = "at MainKt.isPrime(Main.kt:14)\n" + - "\tat MainKt.foo1(Main.kt:32)\n" + - "\tat MainKt.foo3(Main.kt:21)\n" + - "\tat MainKt.foo4(Main.kt:25)\n" + - "\tat MainKt\$main\$1\$t1\$1\$1\$1\$1\$1\$1\$1.invokeSuspend(Main.kt:104)" - ), - TestDumpItem( - name = "Thread2", - stateDesc = "\"Thread2\" daemon prio=5 tid=0x24 nid=NA runnable", - stackTrace = "at MainKt.isPrime(Main.kt:14)\n" + - "\tat MainKt.foo1(Main.kt:32)\n" + - "\tat MainKt.foo3(Main.kt:21)\n" + - "\tat MainKt.foo4(Main.kt:25)\n" + - "\tat MainKt\$main\$1\$t1\$1\$1\$1\$1\$1\$1\$1.invokeSuspend(Main.kt:104)" - ), - TestDumpItem( - name = "Thread3", - stateDesc = "\"Thread3\" daemon prio=5 tid=0x24 nid=NA runnable", - stackTrace = "at MainKt.foo2(_Collections.kt:1915)\n" + - "\tat MainKt\$main\$1\$t2\$1\$1\$1\$1\$1\$1.invokeSuspend(Main.kt:128)\n" + - "\tat MainKt\$main\$1\$t2\$1\$1\$1\$1\$1\$1.invoke(Main.kt:-1)\n" + - "\tat MainKt\$main\$1\$t2\$1\$1\$1\$1\$1\$1.invoke(Main.kt:-1)\n" + - "\tat MainKt.boo(Main.kt:167)\n" + - "\tat MainKt.access\$boo(Main.kt:1)" - ), - ) - } -} - -private class TestDumpItem( - override val name: String, - override val stateDesc: String, - override val stackTrace: @NlsSafe String, -): MergeableDumpItem { - override val interestLevel: Int - get() = stackTrace.count { it == '\n' } - override val icon: Icon - get() = AllIcons.Debugger.ThreadRunning - override val iconToolTip: @Nls String? - get() = null - override val attributes: SimpleTextAttributes - get() = DumpItem.RUNNING_ATTRIBUTES - override val isDeadLocked: Boolean - get() = false - override val awaitingDumpItems: Set - get() = emptySet() - - override val mergeableToken: MergeableToken get() = TestMergeableToken() - - private inner class TestMergeableToken : MergeableToken { - private val comparableStackTrace: String = - stackTrace.substringAfter("\n").replace("<0x\\d+>\\s".toRegex(), "") - - override val item: TestDumpItem get() = this@TestDumpItem - - override fun equals(other: Any?): Boolean { - if (other !is TestMergeableToken) return false - if (this.comparableStackTrace != other.comparableStackTrace) return false - return true - } - - override fun hashCode(): Int { - return Objects.hash( - comparableStackTrace - ) - } - } -} \ No newline at end of file diff --git a/platform/threadDumpParser/src/com/intellij/threadDumpParser/ThreadState.java b/platform/threadDumpParser/src/com/intellij/threadDumpParser/ThreadState.java index 0748bc43ebee..47049db0c038 100644 --- a/platform/threadDumpParser/src/com/intellij/threadDumpParser/ThreadState.java +++ b/platform/threadDumpParser/src/com/intellij/threadDumpParser/ThreadState.java @@ -25,6 +25,8 @@ public class ThreadState { private String myExtraState; private boolean isDaemon; private boolean isVirtual; + private long uniqueId; + private Long threadContainerUniqueId; private final Set myThreadsWaitingForMyLock = new HashSet<>(); private final Set myDeadlockedThreads = new HashSet<>(); private String ownableSynchronizers; @@ -202,4 +204,12 @@ public class ThreadState { public void setVirtual(boolean virtual) { isVirtual = virtual; } + + public long getUniqueId() { return uniqueId; } + + public void setUniqueId(long id) { uniqueId = id; } + + public Long getThreadContainerUniqueId() { return threadContainerUniqueId; } + + public void setThreadContainerUniqueId(Long id) { threadContainerUniqueId = id; } } diff --git a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutinesDumpAsyncProvider.kt b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutinesDumpAsyncProvider.kt index 4d75db3e69d1..c305cfde5bc6 100644 --- a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutinesDumpAsyncProvider.kt +++ b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutinesDumpAsyncProvider.kt @@ -58,6 +58,11 @@ private class CoroutineDumpItem(info: CoroutineInfoData) : MergeableDumpItem { override val name: String = info.name + ":" + info.id + override val id: Long = info.hashCode().toLong() // todo + + override val parentId: Long? + get() = null // todo + override val stateDesc: String = " (${info.state.name.lowercase()})" override val iconToolTip: String @@ -105,6 +110,9 @@ private class CoroutineDumpItem(info: CoroutineInfoData) : MergeableDumpItem { State.CREATED, State.UNKNOWN -> DumpItem.UNINTERESTING_ATTRIBUTES } + override val isContainer: Boolean + get() = false + override val mergeableToken: MergeableToken get() = CoroutinesMergeableToken() private inner class CoroutinesMergeableToken : MergeableToken {