[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

(cherry picked from commit e8f75a53a67bba331dd0e713ec3857bb1604dd29)

IJ-CR-167309

GitOrigin-RevId: 73e4963705f96b1d4542b9c83604a87edadc8378
This commit is contained in:
Vladimir Parfinenko
2025-06-25 16:31:19 +02:00
committed by intellij-monorepo-bot
parent c558318dc3
commit d510e15c98
6 changed files with 102 additions and 45 deletions

View File

@@ -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)
}
}
@@ -540,8 +551,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

View File

@@ -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 <R> tryToBreakOnAnyMethodAndEvaluate (
process: DebugProcessImpl,
pauseSuspendContext: SuspendContextImpl?,
timeToSuspend: Duration,
block: suspend (SuspendContextImpl) -> R
actionToEvaluate: suspend (SuspendContextImpl) -> R
): R {
val onPause = pauseSuspendContext != null
val actionResult = Channel<R>(capacity = 1)
val evaluatableContextObtained = CompletableDeferred<Unit>()
var timedOut = false
val programSuspendedActionStarted = CompletableDeferred<Unit>()
val actionResult = CompletableDeferred<R>()
// 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 <R> 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()
}

View File

@@ -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.

View File

@@ -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

View File

@@ -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<DumpItem>
get() = emptySet()
}

View File

@@ -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") &&