mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-19 21:11:28 +07:00
[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:
committed by
intellij-monorepo-bot
parent
c558318dc3
commit
d510e15c98
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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") &&
|
||||
|
||||
Reference in New Issue
Block a user