From 95e553343536b06ed1627e4aafcb8b0c91391c73 Mon Sep 17 00:00:00 2001 From: Vladimir Parfinenko Date: Wed, 25 Jun 2025 16:31:19 +0200 Subject: [PATCH] [debugger] Thread Dump fixes * explicitly show in UI when some dump items weren't collected * fix duplicated platform threads (in case of any timeouts/errors) * fix data-race during waiting for evaluatable context GitOrigin-RevId: e8f75a53a67bba331dd0e713ec3857bb1604dd29 --- .../debugger/actions/ThreadDumpAction.kt | 55 +++++++++++-------- .../debugger/engine/EvaluationUtils.kt | 50 +++++++++++------ .../impl/ThreadDumpItemsProviderFactory.kt | 6 +- .../messages/JavaDebuggerBundle.properties | 6 +- .../src/com/intellij/unscramble/DumpItem.kt | 28 ++++++++++ .../view/CoroutinesDumpAsyncProvider.kt | 2 +- 6 files changed, 102 insertions(+), 45 deletions(-) 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 bbf8da3e3e7f..c07e1548003c 100644 --- a/java/debugger/impl/src/com/intellij/debugger/actions/ThreadDumpAction.kt +++ b/java/debugger/impl/src/com/intellij/debugger/actions/ThreadDumpAction.kt @@ -25,6 +25,7 @@ import com.intellij.platform.ide.progress.withBackgroundProgress import com.intellij.rt.debugger.VirtualThreadDumper import com.intellij.threadDumpParser.ThreadDumpParser import com.intellij.threadDumpParser.ThreadState +import com.intellij.unscramble.InfoDumpItem import com.intellij.unscramble.MergeableDumpItem import com.intellij.unscramble.toDumpItems import com.intellij.util.lang.JavaVersion @@ -34,7 +35,6 @@ import com.sun.jdi.* import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.NonNls import java.util.concurrent.CancellationException @@ -69,6 +69,9 @@ class ThreadDumpAction { if (onlyPlatformThreads || !Registry.`is`("debugger.thread.dump.extended")) { sendJavaPlatformThreads() + dumpItemsChannel.send(listOf(InfoDumpItem( + JavaDebuggerBundle.message("thread.dump.unavailable.title"), + "Collection of extended dump was disabled."))) DebuggerStatistics.logPlatformThreadDumpFallback( context.project, if (onlyPlatformThreads) ThreadDumpStatus.PLATFORM_DUMP_ALT_CLICK else ThreadDumpStatus.PLATFORM_DUMP_EXTENDED_DUMP_DISABLED ) @@ -78,25 +81,24 @@ class ThreadDumpAction { try { val providers = extendedProviders.extensionList.map { it.getProvider(context) } - suspend fun getAllItems(suspendContext: SuspendContextImpl?) { + suspend fun sendAllItems(suspendContext: SuspendContextImpl?) { coroutineScope { - // Compute parts of the dump asynchronously - launch { - sendJavaPlatformThreads() - } - providers.map { p -> - launch { - withBackgroundProgress(context.project, p.progressText) { - try { - val items = p.getItems(suspendContext) - dumpItemsChannel.send(items) - } - catch (e: CancellationException) { - thisLogger().debug("${p.progressText} was cancelled by user.") - throw e - } + sendJavaPlatformThreads() + + for (p in providers) { + val items = try { + withBackgroundProgress(context.project, + JavaDebuggerBundle.message("thread.dump.progress.message", p.itemsName)) { + p.getItems(suspendContext) } } + catch (@Suppress("IncorrectCancellationExceptionHandling") _: CancellationException) { + thisLogger().debug("Dumping of ${p.itemsName} was cancelled by user.") + listOf(InfoDumpItem( + JavaDebuggerBundle.message("thread.dump.unavailable.title"), + "Dumping of ${p.itemsName} was cancelled.")) + } + dumpItemsChannel.send(items) } } } @@ -107,6 +109,9 @@ class ThreadDumpAction { // If the previous dump is still being evaluated, only show the Java platform thread dump and do not start a new evaluation. if (vm.getUserData(EVALUATION_IN_PROGRESS) == true) { sendJavaPlatformThreads() + dumpItemsChannel.send(listOf(InfoDumpItem( + JavaDebuggerBundle.message("thread.dump.unavailable.title"), + "Previous dump is still in progress."))) DebuggerStatistics.logPlatformThreadDumpFallback(context.project, ThreadDumpStatus.PLATFORM_DUMP_FALLBACK_DURING_EVALUATION) XDebuggerManagerImpl.getNotificationGroup() .createNotification(JavaDebuggerBundle.message("thread.dump.during.previous.dump.evaluation.warning"), NotificationType.INFORMATION) @@ -118,12 +123,15 @@ class ThreadDumpAction { try { vm.putUserData(EVALUATION_IN_PROGRESS, true) suspendAllAndEvaluate(context, timeout) { suspendContext -> - getAllItems(suspendContext) + sendAllItems(suspendContext) } } catch (_: TimeoutCancellationException) { thisLogger().warn("timeout while waiting for evaluatable context ($timeout)") sendJavaPlatformThreads() + dumpItemsChannel.send(listOf(InfoDumpItem( + JavaDebuggerBundle.message("thread.dump.unavailable.title"), + "Timeout while waiting for evaluatable context, unable to dump ${providers.joinToString(", ") { it.itemsName }}."))) DebuggerStatistics.logPlatformThreadDumpFallback(context.project, ThreadDumpStatus.PLATFORM_DUMP_FALLBACK_TIMEOUT) } finally { vm.removeUserData(EVALUATION_IN_PROGRESS) @@ -133,7 +141,7 @@ class ThreadDumpAction { val vm = context.debugProcess!!.virtualMachineProxy vm.suspend() try { - getAllItems(null) + sendAllItems(null) } finally { vm.resume() @@ -145,7 +153,10 @@ class ThreadDumpAction { is CancellationException, is ControlFlowException -> throw e else -> { thisLogger().error(e) - sendJavaPlatformThreads() + // There is no sense to try to send Java platform threads once again. + dumpItemsChannel.send(listOf(InfoDumpItem( + JavaDebuggerBundle.message("thread.dump.unavailable.title"), + "Some internal error happened."))) DebuggerStatistics.logPlatformThreadDumpFallback(context.project, ThreadDumpStatus.PLATFORM_DUMP_FALLBACK_ERROR) } } @@ -532,8 +543,8 @@ private class JavaVirtualThreadsProvider : ThreadDumpItemsProviderFactory() { // Check if VirtualThread class is at least loaded. vm.classesByName("java.lang.VirtualThread").isNotEmpty() - override val progressText: String - get() = JavaDebuggerBundle.message("thread.dump.virtual.threads.progress") + override val itemsName: String + get() = JavaDebuggerBundle.message("thread.dump.virtual.threads.name") override val requiresEvaluation get() = enabled diff --git a/java/debugger/impl/src/com/intellij/debugger/engine/EvaluationUtils.kt b/java/debugger/impl/src/com/intellij/debugger/engine/EvaluationUtils.kt index 4ecaa789e047..07b1220fc679 100644 --- a/java/debugger/impl/src/com/intellij/debugger/engine/EvaluationUtils.kt +++ b/java/debugger/impl/src/com/intellij/debugger/engine/EvaluationUtils.kt @@ -11,6 +11,7 @@ import com.sun.jdi.request.EventRequest import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.completeWith import kotlinx.coroutines.withTimeout import org.jetbrains.annotations.ApiStatus import kotlin.time.Duration @@ -61,22 +62,27 @@ private suspend fun tryToBreakOnAnyMethodAndEvaluate ( process: DebugProcessImpl, pauseSuspendContext: SuspendContextImpl?, timeToSuspend: Duration, - block: suspend (SuspendContextImpl) -> R + actionToEvaluate: suspend (SuspendContextImpl) -> R ): R { val onPause = pauseSuspendContext != null - val actionResult = Channel(capacity = 1) - val evaluatableContextObtained = CompletableDeferred() + var timedOut = false + + val programSuspendedActionStarted = CompletableDeferred() + val actionResult = CompletableDeferred() // Create a request which suspends all the threads and gets the suspendContext. val requestor = object : FilteredRequestor { override fun processLocatableEvent(action: SuspendContextCommandImpl, event: LocatableEvent?): Boolean { - val requestor = this - runBlockingCancellable { - process.requestsManager.deleteRequest(requestor) + process.requestsManager.deleteRequest(this) + if (!timedOut) { val suspendContext = action.suspendContext!! - evaluatableContextObtained.complete(Unit) - actionResult.send(block(suspendContext)) + programSuspendedActionStarted.complete(Unit) + actionResult.completeWith(runCatching { + runBlockingCancellable { + actionToEvaluate(suspendContext) + } + }) } // Note: in case the context was not originally suspended, return false, // so that suspendContext is resumed when action is computed, @@ -100,22 +106,32 @@ private suspend fun tryToBreakOnAnyMethodAndEvaluate ( // Check that we hit the breakpoint within the specified timeout try { withTimeout(timeToSuspend) { - evaluatableContextObtained.await() + programSuspendedActionStarted.await() } } catch (e: TimeoutCancellationException) { - if (onPause) { - // FIXME: get preferred thread from pauseSuspendContext - // If the context was originally on pause, but after resume did not hit a breakpoint within a timeout, - // then it should be paused again - context.managerThread!! - .invokeNow(process.createPauseCommand(null)) + // Try to make it earlier. + process.requestsManager.deleteRequest(requestor) + + withDebugContext(context.managerThread!!) { + // FIXME: unify all this logic with evaluatable Pause + timedOut = true + if (programSuspendedActionStarted.isCompleted) { + // Request was already processed, we need to ignore the timeout. + } else { + if (onPause) { + // FIXME: get preferred thread from pauseSuspendContext + // If the context was originally on pause, but after resume did not hit a breakpoint within a timeout, + // then it should be paused again + process.createPauseCommand(null).invokeCommand() + } + throw e + } } - throw e } finally { process.requestsManager.deleteRequest(requestor) } - return actionResult.receive() + return actionResult.await() } diff --git a/java/debugger/impl/src/com/intellij/debugger/impl/ThreadDumpItemsProviderFactory.kt b/java/debugger/impl/src/com/intellij/debugger/impl/ThreadDumpItemsProviderFactory.kt index 5bff991603c1..6254f50faadc 100644 --- a/java/debugger/impl/src/com/intellij/debugger/impl/ThreadDumpItemsProviderFactory.kt +++ b/java/debugger/impl/src/com/intellij/debugger/impl/ThreadDumpItemsProviderFactory.kt @@ -2,10 +2,10 @@ package com.intellij.debugger.impl import com.intellij.debugger.engine.SuspendContextImpl -import com.intellij.openapi.util.NlsContexts import com.intellij.unscramble.DumpItem import com.intellij.unscramble.MergeableDumpItem import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.Nls /** * A provider of [DumpItem] instances, which may be shown in [com.intellij.unscramble.ThreadDumpPanel]. @@ -18,8 +18,8 @@ abstract class ThreadDumpItemsProviderFactory { @ApiStatus.Internal interface ThreadDumpItemsProvider { - @get:NlsContexts.ProgressTitle - val progressText: String + @get:Nls + val itemsName: String /** * Returns whether this provider requires [SuspendContextImpl] which can be used to evaluate some information to provide dump items. diff --git a/java/debugger/openapi/resources/messages/JavaDebuggerBundle.properties b/java/debugger/openapi/resources/messages/JavaDebuggerBundle.properties index 981282187ef2..bb484440c37a 100644 --- a/java/debugger/openapi/resources/messages/JavaDebuggerBundle.properties +++ b/java/debugger/openapi/resources/messages/JavaDebuggerBundle.properties @@ -474,8 +474,10 @@ export.selected.capture.points.to.file=Export Selected Capture Points to File\u2 import.capture.points=Import Capture Points please.select.a.file.to.import=Please select a file to import. waiting.for.debugger.response=Waiting for the process to finish gracefully -thread.dump.virtual.threads.progress=Dumping Java virtual threads\u2026 -thread.dump.coroutines.progress=Dumping Coroutines\u2026 +thread.dump.progress.message=Dumping {0}\u2026 +thread.dump.virtual.threads.name=Java virtual threads +thread.dump.coroutines.name=Coroutines +thread.dump.unavailable.title=Extended dump unavailable collection.history=Collection history cancel.emulation=Cancel emulation thread.operation.interrupt.is.not.supported.by.vm=Thread operation 'interrupt' is not supported by VM 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 29db61465469..077e67427234 100644 --- a/java/java-frontback-impl/src/com/intellij/unscramble/DumpItem.kt +++ b/java/java-frontback-impl/src/com/intellij/unscramble/DumpItem.kt @@ -245,3 +245,31 @@ private class JavaThreadDumpItem(private val threadState: ThreadState) : Mergeab } } +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) + override fun hashCode() = super.hashCode() + override val item = this@InfoDumpItem + } + + override val name: @NlsSafe String + get() = title + override val stateDesc: @NlsSafe String + get() = "" + override val stackTrace: @NlsSafe String + get() = details + override val interestLevel: Int + get() = Int.MIN_VALUE + override val icon: Icon + get() = AllIcons.General.Information + override val iconToolTip: @Nls String? + get() = null + override val attributes: SimpleTextAttributes + get() = SimpleTextAttributes.REGULAR_ATTRIBUTES + override val isDeadLocked: Boolean + get() = false + override val awaitingDumpItems: Set + get() = emptySet() + +} + 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 00dfd812cd9d..f576079ecc92 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 @@ -27,7 +27,7 @@ import javax.swing.Icon @ApiStatus.Internal class CoroutinesDumpAsyncProvider : ThreadDumpItemsProviderFactory() { override fun getProvider(context: DebuggerContextImpl): ThreadDumpItemsProvider = object : ThreadDumpItemsProvider { - override val progressText: String get() = JavaDebuggerBundle.message("thread.dump.coroutines.progress") + override val itemsName: String get() = JavaDebuggerBundle.message("thread.dump.coroutines.name") private val enabled: Boolean = Registry.`is`("debugger.kotlin.show.coroutines.in.threadDumpPanel") &&