[debugger] Asynchronously compute dumps of coroutines and Java threads and show in ThreadDumpPanel as soon as ready

IDEA-369744
IDEA-369956

GitOrigin-RevId: 72b33d78951b8fbea68d5b1c6b4ee8b72fd6136b
This commit is contained in:
Maria Sokolova
2025-04-10 14:58:04 +00:00
committed by intellij-monorepo-bot
parent fbeb351d2e
commit 76796255a4
7 changed files with 108 additions and 95 deletions
@@ -2,16 +2,11 @@
package com.intellij.debugger.actions
import com.intellij.debugger.DebuggerManagerEx
import com.intellij.debugger.JavaDebuggerBundle
import com.intellij.debugger.engine.*
import com.intellij.debugger.engine.MethodInvokeUtils.getMethodHandlesImplLookup
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.impl.DebuggerUtilsImpl
import com.intellij.debugger.impl.ThreadDumpItemsProvider
import com.intellij.debugger.impl.ThreadDumpItemsProviderFactory
import com.intellij.debugger.impl.*
import com.intellij.debugger.jdi.VirtualMachineProxyImpl
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
@@ -32,9 +27,8 @@ import com.intellij.unscramble.JavaThreadDumpItem
import com.intellij.util.lang.JavaVersion
import com.jetbrains.jdi.ThreadReferenceImpl
import com.sun.jdi.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.withContext
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import org.jetbrains.annotations.NonNls
import java.lang.Long as JLong
import java.util.concurrent.CancellationException
@@ -45,8 +39,13 @@ import kotlin.checkNotNull
import kotlin.let
import kotlin.time.Duration.Companion.milliseconds
import kotlin.to
import com.intellij.openapi.project.Project
import org.jetbrains.annotations.ApiStatus
import com.intellij.debugger.JavaDebuggerBundle
class ThreadDumpAction : DumbAwareAction() {
@OptIn(ExperimentalCoroutinesApi::class)
override fun actionPerformed(e: AnActionEvent) {
val project = e.project
if (project == null) {
@@ -58,12 +57,12 @@ class ThreadDumpAction : DumbAwareAction() {
val managerThread = context.managerThread!!
if (session != null && session.isAttached) {
executeOnDMT(managerThread) {
val dumpItems = buildThreadDump(context)
withContext(Dispatchers.EDT) {
val xSession = session.xDebugSession
if (xSession != null) {
DebuggerUtilsEx.addDumpItems(project, dumpItems, xSession.ui, session.searchScope)
}
// Pass parts of the dump to the ThreadDumpPanel via a channel as soon as they are computed
val dumpItemsChannel = produce(capacity = Channel.BUFFERED) {
buildThreadDump(context, channel)
}
launch(Dispatchers.EDT) {
collectAndShowDumpItems(project, session, dumpItemsChannel)
}
}
}
@@ -99,31 +98,46 @@ class ThreadDumpAction : DumbAwareAction() {
"(" + DebuggerUtilsEx.getSourceName(location, "Unknown Source") + ":" + DebuggerUtilsEx.getLineNumber(location, false) + ")"
}
private suspend fun buildThreadDump(context: DebuggerContextImpl): List<DumpItem> {
fun fallback() =
buildJavaPlatformThreadDump(context).map(::JavaThreadDumpItem)
@ApiStatus.Internal
suspend fun buildThreadDump(context: DebuggerContextImpl, dumpItemsChannel: SendChannel<List<DumpItem>>) {
suspend fun fallback() =
dumpItemsChannel.send(
buildJavaPlatformThreadDump(context).map(::JavaThreadDumpItem)
)
if (!Registry.`is`("debugger.thread.dump.extended")) {
return fallback()
fallback()
return
}
return try {
try {
val providers = extendedProviders.extensionList.map { it.getProvider(context) }
suspend fun getAllItems(suspendContext: SuspendContextImpl?) =
withBackgroundProgress(context.project, JavaDebuggerBundle.message("thread.dump.extended.progress")) {
providers
.flatMap { it.getItems(suspendContext) }
.sortedWith(DumpItem.BY_INTEREST)
suspend fun getAllItems(suspendContext: SuspendContextImpl?) {
coroutineScope {
// Compute parts of the dump asynchronously
providers.map { p ->
launch {
withBackgroundProgress(context.project, p.progressText) {
try {
val items = p.getItems(suspendContext).sortedWith(DumpItem.BY_INTEREST)
dumpItemsChannel.send(items)
}
catch (e: CancellationException) {
thisLogger().debug("${p.progressText} was cancelled by user.")
throw e
}
}
}
}
}
}
if (providers.any { it.requiresEvaluation }) {
val timeout = Registry.intValue("debugger.thread.dump.suspension.timeout.ms", 500).milliseconds
try {
suspendAllAndEvaluate(context, timeout) { suspendContext ->
withDebugContext(suspendContext) {
getAllItems(suspendContext)
}
getAllItems(suspendContext)
}
}
catch (_: TimeoutCancellationException) {
@@ -135,7 +149,7 @@ class ThreadDumpAction : DumbAwareAction() {
val vm = context.debugProcess!!.virtualMachineProxy
vm.suspend()
try {
return getAllItems(null)
getAllItems(null)
}
finally {
vm.resume()
@@ -165,6 +179,17 @@ class ThreadDumpAction : DumbAwareAction() {
vm.resume()
}
}
private suspend fun collectAndShowDumpItems(project: Project, session: DebuggerSession, dumpItemsChannel: ReceiveChannel<List<DumpItem>>) {
val xSession = session.xDebugSession
if (xSession != null) {
val threadDumpPanel = DebuggerUtilsEx.createThreadDumpPanel(project, emptyList(), xSession.ui, session.searchScope)
for (items in dumpItemsChannel) {
threadDumpPanel.addDumpItems(items)
}
}
}
}
}
@@ -486,6 +511,10 @@ private class JavaThreadsProvider : ThreadDumpItemsProviderFactory() {
// Check if VirtualThread class is at least loaded.
vm.classesByName("java.lang.VirtualThread").isNotEmpty()
override val progressText: String get() = JavaDebuggerBundle.message(
if (shouldDumpVirtualThreads) "thread.dump.platform.and.virtual.threads.progress" else "thread.dump.platform.threads.progress"
)
override val requiresEvaluation get() = shouldDumpVirtualThreads
override fun getItems(suspendContext: SuspendContextImpl?): List<DumpItem> {
@@ -5,8 +5,10 @@ import com.intellij.debugger.engine.events.SuspendContextCommandImpl
import com.intellij.debugger.impl.DebuggerContextImpl
import com.intellij.debugger.settings.DebuggerSettings
import com.intellij.debugger.ui.breakpoints.FilteredRequestor
import com.intellij.openapi.progress.runBlockingCancellable
import com.sun.jdi.event.LocatableEvent
import com.sun.jdi.request.EventRequest
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.withTimeout
@@ -29,8 +31,7 @@ internal suspend fun <R> suspendAllAndEvaluate(
val suspendContext = context.suspendContext
return if (suspendContext == null) {
// Not suspended at all.
tryToBreakOnAnyMethodAndEvaluate(context, process, timeToSuspend, action)
tryToBreakOnAnyMethodAndEvaluate(context, process, null, timeToSuspend, action)
}
else if (process.isEvaluationPossible(suspendContext)) {
if (suspendContext.suspendPolicy == EventRequest.SUSPEND_EVENT_THREAD) {
@@ -50,27 +51,36 @@ internal suspend fun <R> suspendAllAndEvaluate(
}
else {
// We are on a pause, cannot evaluate.
tryToResumeThenBreakOnAnyMethodAndEvaluate(context, process, suspendContext, timeToSuspend, action)
tryToBreakOnAnyMethodAndEvaluate(context, process, suspendContext, timeToSuspend, action)
}
}
// FIXME: too much copypasted, compare and merge
private suspend fun <R> tryToBreakOnAnyMethodAndEvaluate(
private suspend fun <R> tryToBreakOnAnyMethodAndEvaluate (
context: DebuggerContextImpl,
process: DebugProcessImpl,
pauseSuspendContext: SuspendContextImpl?,
timeToSuspend: Duration,
action: suspend (SuspendContextImpl) -> R
block: suspend (SuspendContextImpl) -> R
): R {
val evaluatableContextResult = Channel<SuspendContextImpl>(capacity = 1)
val onPause = pauseSuspendContext != null
val actionResult = Channel<R>(capacity = 1)
val evaluatableContextObtained = CompletableDeferred<Unit>()
// Create a request which suspends all the threads and gets the suspendContext.
val requestor = object : FilteredRequestor {
override fun processLocatableEvent(action: SuspendContextCommandImpl, event: LocatableEvent?): Boolean {
process.requestsManager.deleteRequest(this)
val evaluatableContext = action.suspendContext!!
evaluatableContextResult.trySend(evaluatableContext).also { assert(it.isSuccess) }
return true
val requestor = this
runBlockingCancellable {
process.requestsManager.deleteRequest(requestor)
val suspendContext = action.suspendContext!!
evaluatableContextObtained.complete(Unit)
actionResult.send(block(suspendContext))
}
// Note: in case the context was not originally suspended, return false,
// so that suspendContext is resumed when action is computed,
// thus no suspension will be visible in the UI
return onPause
}
override fun getSuspendPolicy(): String = DebuggerSettings.SUSPEND_ALL
@@ -80,66 +90,31 @@ private suspend fun <R> tryToBreakOnAnyMethodAndEvaluate(
request.setSuspendPolicy(EventRequest.SUSPEND_ALL)
request.isEnabled = true
val evaluatableContext = try {
withTimeout(timeToSuspend) {
evaluatableContextResult.receive()
}
}
finally {
process.requestsManager.deleteRequest(requestor)
}
try {
return action(evaluatableContext)
}
finally {
// If the context was on pause, it should be resume first to hit the breakpoint
if (onPause) {
context.managerThread!!
.invokeNow(process.createResumeCommand(evaluatableContext))
}
}
private suspend fun <R> tryToResumeThenBreakOnAnyMethodAndEvaluate(
context: DebuggerContextImpl,
process: DebugProcessImpl,
pauseSuspendContext: SuspendContextImpl,
timeToSuspend: Duration,
action: suspend (SuspendContextImpl) -> R
): R {
val evaluatableContextResult = Channel<SuspendContextImpl>(capacity = 1)
// Create a request which suspends all the threads and gets the suspendContext.
val requestor = object : FilteredRequestor {
override fun processLocatableEvent(action: SuspendContextCommandImpl, event: LocatableEvent?): Boolean {
process.requestsManager.deleteRequest(this)
val evaluatableContext = action.suspendContext!!
evaluatableContextResult.trySend(evaluatableContext).also { assert(it.isSuccess) }
return true
}
override fun getSuspendPolicy(): String = DebuggerSettings.SUSPEND_ALL
.invokeNow(process.createResumeCommand(pauseSuspendContext))
}
val request = process.requestsManager.createMethodEntryRequest(requestor)
request.setSuspendPolicy(EventRequest.SUSPEND_ALL)
request.isEnabled = true
context.managerThread!!
.invokeNow(process.createResumeCommand(pauseSuspendContext))
val evaluatableContext = try {
// Check that we hit the breakpoint within the specified timeout
try {
withTimeout(timeToSuspend) {
evaluatableContextResult.receive()
evaluatableContextObtained.await()
}
}
catch (e: TimeoutCancellationException) {
// FIXME: get preferred thread from pauseSuspendContext
context.managerThread!!
.invokeNow(process.createPauseCommand(null))
throw e
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))
throw e
}
}
finally {
process.requestsManager.deleteRequest(requestor)
}
return action(evaluatableContext)
return actionResult.receive()
}
@@ -3,6 +3,7 @@ package com.intellij.debugger.impl
import com.intellij.unscramble.DumpItem
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.openapi.util.NlsContexts
import org.jetbrains.annotations.ApiStatus
/**
@@ -16,6 +17,9 @@ abstract class ThreadDumpItemsProviderFactory {
@ApiStatus.Internal
interface ThreadDumpItemsProvider {
@get:NlsContexts.ProgressTitle
val progressText: String
/**
* Returns whether this provider requires [SuspendContextImpl] which can be used to evaluate some information to provide dump items.
*/
@@ -472,7 +472,9 @@ 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=Thread Dump
thread.dump.extended.progress=Collecting extended thread dump
thread.dump.platform.threads.progress=Dumping Java platform threads\u2026
thread.dump.platform.and.virtual.threads.progress=Dumping Java platform threads and virtual threads\u2026
thread.dump.coroutines.progress=Dumping Coroutines\u2026
collection.history=Collection history
cancel.emulation=Cancel emulation
thread.operation.interrupt.is.not.supported.by.vm=Thread operation 'interrupt' is not supported by VM
@@ -72,7 +72,7 @@ object CoroutineBreakpointFacility {
override fun isRestoreBreakpoints(): Boolean = false
override fun stopOnlyInBaseClass(): Boolean = true
override fun processLocatableEvent(action: SuspendContextCommandImpl, event: LocatableEvent): Boolean {
override fun processLocatableEvent(action: SuspendContextCommandImpl, event: LocatableEvent?): Boolean {
thisLogger().debug { "Hit the resume breakpoint at ${context.location}" }
val result = super.processLocatableEvent(action, event)
if (result) {
@@ -159,7 +159,7 @@ class KotlinLambdaAsyncMethodFilter(
private val lambdaReference: ObjectReference,
lambdaMethod: Method
) : StepIntoMethodBreakpoint(lambdaMethod.declaringType().name(), lambdaMethod.name(), lambdaMethod.signature(), context.debugProcess.project) {
override fun processLocatableEvent(action: SuspendContextCommandImpl, event: LocatableEvent): Boolean {
override fun processLocatableEvent(action: SuspendContextCommandImpl, event: LocatableEvent?): Boolean {
super.processLocatableEvent(action, event).also { stopped ->
if (!stopped) return false
context.debugProcess.requestsManager.deleteRequest(this)
@@ -1,6 +1,7 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.debugger.coroutine.view
import com.intellij.debugger.JavaDebuggerBundle
import com.intellij.debugger.actions.ThreadDumpAction
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.impl.DebuggerContextImpl
@@ -28,6 +29,8 @@ import com.intellij.unscramble.MergeableToken
@ApiStatus.Internal
class CoroutinesDumpAsyncProvider : ThreadDumpItemsProviderFactory() {
override fun getProvider(context: DebuggerContextImpl): ThreadDumpItemsProvider = object : ThreadDumpItemsProvider {
override val progressText: String get() = JavaDebuggerBundle.message("thread.dump.coroutines.progress")
private val vm = context.debugProcess!!.virtualMachineProxy
private val enabled =