mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[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:
committed by
intellij-monorepo-bot
parent
fbeb351d2e
commit
76796255a4
@@ -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
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+3
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user