[debugger] Changed XDebugSessionProxy#computeRunningExecutionStacks to be able to provide grouped execution stacks

IJPL-223919

GitOrigin-RevId: fbdc6d3f40a859c721f899cbdad76587670e30aa
This commit is contained in:
Maria Sokolova
2026-02-03 13:42:25 +00:00
committed by intellij-monorepo-bot
parent 396e67581b
commit bddcbed96c
10 changed files with 222 additions and 33 deletions
@@ -25,6 +25,7 @@ import com.intellij.debugger.jdi.VirtualMachineProxyImpl;
import com.intellij.debugger.memory.component.MemoryViewDebugProcessData;
import com.intellij.debugger.memory.ui.ClassesFilteredView;
import com.intellij.debugger.settings.DebuggerSettings;
import com.intellij.debugger.settings.ThreadsViewSettings;
import com.intellij.debugger.ui.AlternativeSourceNotificationProvider;
import com.intellij.debugger.ui.DebuggerContentInfo;
import com.intellij.debugger.ui.breakpoints.Breakpoint;
@@ -95,6 +96,8 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.java.debugger.JavaDebuggerEditorsProvider;
import java.util.concurrent.CompletableFuture;
public class JavaDebugProcess extends XDebugProcess {
private final DebuggerSession myJavaSession;
private final JavaDebuggerEditorsProvider myEditorsProvider;
@@ -291,7 +294,13 @@ public class JavaDebugProcess extends XDebugProcess {
}
@Override
public void computeRunningExecutionStacks(XSuspendContext.XExecutionStackContainer container) {
public void computeRunningExecutionStacks(XSuspendContext.XExecutionStackGroupContainer container, @Nullable XSuspendContext suspendContext) {
// TODO: Provide a platform-level way to define threads view settings, IDEA-384653
var showThreadGroups = ThreadsViewSettings.getInstance().SHOW_THREAD_GROUPS;
if (!showThreadGroups && suspendContext != null) {
suspendContext.computeExecutionStacks(container);
return;
}
var debugProcess = getDebuggerSession().getProcess();
var context = debugProcess.getDebuggerContext();
var managerThread = context.getManagerThread();
@@ -303,12 +312,11 @@ public class JavaDebugProcess extends XDebugProcess {
@Override
protected void action() {
try {
var currentThread = context.getThreadProxy();
var allThreads = VirtualMachineProxyImpl.getCurrent().allThreads();
var executionStacks = ContainerUtil.map(
allThreads, (thread) -> (XExecutionStack) new JavaExecutionStack(thread, debugProcess, thread.equals(currentThread))
);
container.addExecutionStack(executionStacks, true);
if (showThreadGroups) {
addExecutionStackGroups(container, debugProcess);
} else {
addRunningExecutionStacks(container, debugProcess);
}
}
catch (Throwable e) {
container.errorOccurred(XDebuggerBundle.message("debugger.threads.not.available") + ": " + e.getMessage());
@@ -322,6 +330,30 @@ public class JavaDebugProcess extends XDebugProcess {
});
}
private static void addExecutionStackGroups(XSuspendContext.XExecutionStackGroupContainer container, DebugProcessImpl debugProcess) {
var vm = VirtualMachineProxyImpl.getCurrent();
var currentThread = debugProcess.getDebuggerContext().getThreadProxy();
var executionStackGroups = ContainerUtil.map(
vm.topLevelThreadGroups(),
t -> JavaThreadGroup.buildJavaThreadGroup(t, debugProcess, currentThread)
);
CompletableFuture.allOf(executionStackGroups.toArray(CompletableFuture[]::new))
.thenAccept(unused -> {
if (container.isObsolete()) return;
var groups = ContainerUtil.map(executionStackGroups, CompletableFuture::join);
container.addExecutionStackGroups(groups, true);
})
.exceptionally(DebuggerUtilsAsync::logError);
}
private static void addRunningExecutionStacks(XSuspendContext.XExecutionStackGroupContainer container, DebugProcessImpl debugProcess) {
var allThreads = VirtualMachineProxyImpl.getCurrent().allThreads();
var executionStacks = ContainerUtil.map(
allThreads, (thread) -> (XExecutionStack) new JavaExecutionStack(thread, debugProcess, false)
);
container.addExecutionStack(executionStacks, true);
}
@Override
public @NotNull XDebuggerEditorsProvider getEditorsProvider() {
return myEditorsProvider;
@@ -0,0 +1,37 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.debugger.engine
import com.intellij.debugger.jdi.ThreadGroupReferenceProxyImpl
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl
import com.intellij.icons.AllIcons
import com.intellij.xdebugger.frame.XExecutionStackGroup
import org.jetbrains.annotations.ApiStatus
import java.util.concurrent.CompletableFuture
import javax.swing.Icon
private class JavaThreadGroup(
name: String,
override val groups: List<JavaThreadGroup>,
override val stacks: List<JavaExecutionStack>
) : XExecutionStackGroup(name) {
override val icon: Icon
get() = AllIcons.Debugger.ThreadGroup
companion object {
// Here we load all the groups at once. For Java groups it's not a problem, there are few,
// but if virtual thread groups are introduced to XThreadsView, execution stacks should be loaded lazily.
@ApiStatus.Internal
@JvmStatic
fun buildJavaThreadGroup(group: ThreadGroupReferenceProxyImpl, debugProcess: DebugProcessImpl, currentThread: ThreadReferenceProxyImpl?): CompletableFuture<JavaThreadGroup> {
val name = group.name()
val subGroups = group.threadGroups().map { buildJavaThreadGroup(it, debugProcess, currentThread) }
val stacks = group.threads().map { thread -> JavaExecutionStack.create(thread, debugProcess, thread == currentThread) }
return CompletableFuture.allOf(*(subGroups + stacks).toTypedArray())
.thenApply {
val subGroups = subGroups.mapNotNull { it.join() }
val stacks = stacks.mapNotNull { it.join() }
JavaThreadGroup(name, subGroups, stacks)
}
}
}
}
@@ -12,10 +12,7 @@ import com.intellij.xdebugger.breakpoints.XBreakpoint;
import com.intellij.xdebugger.breakpoints.XBreakpointHandler;
import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider;
import com.intellij.xdebugger.evaluation.XDebuggerEvaluator;
import com.intellij.xdebugger.frame.XDropFrameHandler;
import com.intellij.xdebugger.frame.XStackFrame;
import com.intellij.xdebugger.frame.XSuspendContext;
import com.intellij.xdebugger.frame.XValueMarkerProvider;
import com.intellij.xdebugger.frame.*;
import com.intellij.xdebugger.mixedMode.XMixedModeDebugProcessExtension;
import com.intellij.xdebugger.stepping.XSmartStepIntoHandler;
import com.intellij.xdebugger.ui.XDebugTabLayouter;
@@ -306,7 +303,7 @@ public abstract class XDebugProcess {
* Provide execution stacks corresponding to all the live threads in the debug process to the {@code container}.
*/
@ApiStatus.Internal
public void computeRunningExecutionStacks(XSuspendContext.XExecutionStackContainer container) {
public void computeRunningExecutionStacks(XSuspendContext.XExecutionStackGroupContainer container, @Nullable XSuspendContext suspendContext) {
container.addExecutionStack(List.of(), true);
}
@@ -28,6 +28,7 @@ import com.intellij.xdebugger.XSourcePosition
import com.intellij.xdebugger.evaluation.EvaluationMode
import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider
import com.intellij.xdebugger.frame.XExecutionStack
import com.intellij.xdebugger.frame.XExecutionStackGroup
import com.intellij.xdebugger.frame.XStackFrame
import com.intellij.xdebugger.frame.XSuspendContext
import com.intellij.xdebugger.impl.XDebugSessionImpl
@@ -56,6 +57,7 @@ import kotlinx.coroutines.withContext
import org.jetbrains.concurrency.Promise
import org.jetbrains.concurrency.await
import org.jetbrains.concurrency.rejectedPromise
import kotlin.collections.map
internal class BackendXDebugSessionApi : XDebugSessionApi {
override suspend fun createDocument(frontendDocumentId: FrontendDocumentId, sessionId: XDebugSessionId, expression: XExpressionDto, sourcePosition: XSourcePositionDto?, evaluationMode: EvaluationMode): XExpressionDocumentDto? {
@@ -212,11 +214,12 @@ internal class BackendXDebugSessionApi : XDebugSessionApi {
}
}
override suspend fun computeRunningExecutionStacks(sessionId: XDebugSessionId): Flow<XExecutionStacksEvent> {
override suspend fun computeRunningExecutionStacks(sessionId: XDebugSessionId, suspendContextId: XSuspendContextId?): Flow<XExecutionStackGroupsEvent> {
val session = sessionId.findValue() ?: return emptyFlow()
val suspendContextModel = suspendContextId?.findValue()
val scope = session.coroutineScope.childScopeCancelledOnSessionEvents("RunningExecutionStacksScope", session)
return createExecutionStacksEventFlow(session, scope) { container ->
session.debugProcess.computeRunningExecutionStacks(container)
return createExecutionStackGroupEventFlow(session, scope) { container ->
session.debugProcess.computeRunningExecutionStacks(container, suspendContextModel?.suspendContext)
}
}
@@ -247,14 +250,14 @@ internal class BackendXDebugSessionApi : XDebugSessionApi {
val stacks = executionStacks.map { stack ->
stack.toRpc(scope, session)
}
trySend(XExecutionStacksEvent.NewExecutionStacks(stacks, last))
trySend(NewExecutionStacksEvent(stacks, last))
if (last) {
this@channelFlow.close()
}
}
override fun errorOccurred(errorMessage: @NlsContexts.DialogMessage String) {
trySend(XExecutionStacksEvent.ErrorOccurred(errorMessage))
trySend(ErrorOccurredEvent(errorMessage))
}
}
computeExecutionStacks(container)
@@ -265,6 +268,54 @@ internal class BackendXDebugSessionApi : XDebugSessionApi {
}.buffer(Channel.UNLIMITED)
}
private fun createExecutionStackGroupEventFlow(
session: XDebugSessionImpl,
scope: CoroutineScope,
computeExecutionStacks: (XSuspendContext.XExecutionStackGroupContainer) -> Unit
) : Flow<XExecutionStackGroupsEvent> {
return channelFlow {
attachAsChildTo(scope)
val container = object : XSuspendContext.XExecutionStackGroupContainer {
@Volatile
var obsolete = false
override fun isObsolete(): Boolean {
return obsolete
}
override fun addExecutionStack(executionStacks: List<XExecutionStack>, last: Boolean) {
val stacks = executionStacks.map { stack ->
stack.toRpc(scope, session)
}
trySend(NewExecutionStacksEvent(stacks, last))
if (last) {
this@channelFlow.close()
}
}
override fun addExecutionStackGroups(executionStackGroups: List<XExecutionStackGroup>, last: Boolean) {
val groups = executionStackGroups.map { group ->
group.toRpc(scope, session)
}
trySend(NewExecutionStackGroupsEvent(groups, last))
if (last) {
this@channelFlow.close()
}
}
override fun errorOccurred(errorMessage: @NlsContexts.DialogMessage String) {
trySend(ErrorOccurredEvent(errorMessage))
}
}
computeExecutionStacks(container)
awaitClose {
container.obsolete = true
}
}.buffer(Channel.UNLIMITED)
}
// TODO use a util function from the shared module
private fun CoroutineScope.childScopeCancelledOnSessionEvents(name: String, session: XDebugSessionImpl): CoroutineScope =
childScope(name).also { childScope ->
val listener = object : XDebugSessionListener {
@@ -374,6 +425,15 @@ internal fun XExecutionStack.toRpc(coroutineScope: CoroutineScope, session: XDeb
)
}
internal fun XExecutionStackGroup.toRpc(coroutineScope: CoroutineScope, session: XDebugSessionImpl): XExecutionStackGroupDto {
return XExecutionStackGroupDto(
groups.map { it.toRpc(coroutineScope, session) },
stacks.map { it.toRpc(coroutineScope, session) },
this.name,
this.icon?.rpcId(),
)
}
private fun XStackFrame.captionInfo(): XStackFrameCaptionInfo {
return if (this is XStackFrameWithSeparatorAbove) {
XStackFrameCaptionInfo(hasSeparatorAbove(), captionAboveOf)
@@ -16,12 +16,14 @@ import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.project.Project
import com.intellij.platform.debugger.impl.frontend.evaluate.quick.FrontendXValue
import com.intellij.platform.debugger.impl.frontend.frame.*
import com.intellij.platform.debugger.impl.frontend.storage.FrontendXStackFramesStorage
import com.intellij.platform.debugger.impl.frontend.storage.getOrCreateStackFrame
import com.intellij.platform.debugger.impl.rpc.*
import com.intellij.platform.debugger.impl.shared.proxy.XBreakpointProxy
import com.intellij.platform.debugger.impl.shared.proxy.XDebugSessionProxy
import com.intellij.platform.debugger.impl.shared.proxy.XSmartStepIntoHandlerEntry
import com.intellij.platform.debugger.impl.shared.proxy.XStackFramesListColorsCache
import com.intellij.platform.debugger.impl.shared.childScopeCancelledOnSessionEvents
import com.intellij.platform.execution.impl.frontend.createFrontendProcessHandler
import com.intellij.platform.execution.impl.frontend.executionEnvironment
import com.intellij.platform.util.coroutines.childScope
@@ -37,6 +39,7 @@ import com.intellij.xdebugger.frame.XDropFrameHandler
import com.intellij.xdebugger.frame.XExecutionStack
import com.intellij.xdebugger.frame.XStackFrame
import com.intellij.xdebugger.frame.XSuspendContext
import com.intellij.xdebugger.frame.XSuspendContext.XExecutionStackContainer
import com.intellij.xdebugger.impl.XSourceKind
import com.intellij.xdebugger.impl.frame.XValueMarkers
import com.intellij.xdebugger.impl.inline.DebuggerInlayListener
@@ -446,15 +449,23 @@ class FrontendXDebuggerSession(
return currentContext.isStepping
}
override fun computeExecutionStacks(container: XSuspendContext.XExecutionStackContainer) {
override fun computeExecutionStacks(container: XExecutionStackContainer) {
getCurrentSuspendContext()?.computeExecutionStacks(container)
}
override fun computeRunningExecutionStacks(container: XSuspendContext.XExecutionStackContainer) {
coroutineScope.launch {
override fun computeRunningExecutionStacks(container: XSuspendContext.XExecutionStackGroupContainer) {
val suspendContext = getCurrentSuspendContext()
val scope = suspendContext?.lifetimeScope ?: coroutineScope.childScopeForRunningExecutionStack()
scope.launch {
XDebugSessionApi.getInstance()
.computeRunningExecutionStacks(id)
.collectExecutionStackEvents(project, coroutineScope, container)
.computeRunningExecutionStacks(id, suspendContext?.id)
.collectExecutionStackGroupEvents(project, scope, container)
}
}
private fun CoroutineScope.childScopeForRunningExecutionStack(): CoroutineScope {
return coroutineScope.childScopeCancelledOnSessionEvents("FrontendRunningExecutionStacksScope", this@FrontendXDebuggerSession).also {
coroutineContext.plus(FrontendXStackFramesStorage())
}
}
@@ -609,3 +620,28 @@ private fun <T> CoroutineScope.syncWithLocalFlow(sourceFlow: Flow<T>, localFlowS
}
}
private suspend fun Flow<XExecutionStackGroupsEvent>.collectExecutionStackGroupEvents(
project: Project,
coroutineScope: CoroutineScope,
container: XSuspendContext.XExecutionStackGroupContainer
) {
collect { executionStackEvent ->
when (executionStackEvent) {
is ErrorOccurredEvent -> {
container.errorOccurred(executionStackEvent.errorMessage)
}
is NewExecutionStacksEvent -> {
val feStacks = executionStackEvent.stacks.map { FrontendXExecutionStack(it, project, coroutineScope) }
container.addExecutionStack(feStacks, executionStackEvent.last)
}
is NewExecutionStackGroupsEvent -> {
val feStackGroups = executionStackEvent.groups.map {
FrontendXExecutionStackGroup(it, project, coroutineScope)
}
container.addExecutionStackGroups(feStackGroups, executionStackEvent.last)
}
}
}
}
@@ -4,6 +4,8 @@ package com.intellij.platform.debugger.impl.frontend.frame
import com.intellij.openapi.project.Project
import com.intellij.platform.debugger.impl.frontend.storage.FrontendXStackFramesStorage
import com.intellij.platform.debugger.impl.frontend.storage.getOrCreateStackFrame
import com.intellij.platform.debugger.impl.rpc.ErrorOccurredEvent
import com.intellij.platform.debugger.impl.rpc.NewExecutionStacksEvent
import com.intellij.platform.debugger.impl.rpc.XDebugSessionApi
import com.intellij.platform.debugger.impl.rpc.XExecutionStacksEvent
import com.intellij.platform.debugger.impl.rpc.XStackFrameDto
@@ -54,17 +56,17 @@ internal class FrontendXSuspendContext(
}
}
internal suspend fun Flow<XExecutionStacksEvent>.collectExecutionStackEvents(
private suspend fun Flow<XExecutionStacksEvent>.collectExecutionStackEvents(
project: Project,
coroutineScope: CoroutineScope,
container: XSuspendContext.XExecutionStackContainer
) {
collect { executionStackEvent ->
when (executionStackEvent) {
is XExecutionStacksEvent.ErrorOccurred -> {
is ErrorOccurredEvent -> {
container.errorOccurred(executionStackEvent.errorMessage)
}
is XExecutionStacksEvent.NewExecutionStacks -> {
is NewExecutionStacksEvent -> {
// TODO[IJPL-177087]: here we are binding FrontendXExecutionStack to the suspend context scope,
// which is the safest-narrowest scope in our possession.
// However, maybe it's possible to set up, for example, a scope that ends when another stack is selected from a combobox.
@@ -66,7 +66,7 @@ interface XDebugSessionApi : RemoteApi<Unit> {
suspend fun computeExecutionStacks(suspendContextId: XSuspendContextId): Flow<XExecutionStacksEvent>
suspend fun computeRunningExecutionStacks(sessionId: XDebugSessionId): Flow<XExecutionStacksEvent>
suspend fun computeRunningExecutionStacks(sessionId: XDebugSessionId, suspendContextId: XSuspendContextId?): Flow<XExecutionStackGroupsEvent>
suspend fun muteBreakpoints(sessionDataId: XDebugSessionDataId, muted: Boolean)
@@ -1,6 +1,12 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.debugger.impl.shared
import com.intellij.platform.debugger.impl.shared.proxy.XDebugSessionProxy
import com.intellij.platform.util.coroutines.childScope
import com.intellij.util.asDisposable
import com.intellij.xdebugger.XDebugSessionListener
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -20,4 +26,17 @@ fun <T, R> mapFlow(f: Flow<T>, mapper: (T) -> R): Flow<R> = f.map(mapper)
@ApiStatus.Internal
fun <T> createMutableSharedFlow(replay: Int, extraBufferCapacity: Int): MutableSharedFlow<T> {
return MutableSharedFlow(replay, extraBufferCapacity)
}
}
@ApiStatus.Internal
fun CoroutineScope.childScopeCancelledOnSessionEvents(name: String, session: XDebugSessionProxy): CoroutineScope =
childScope(name).also { childScope ->
val listener = object : XDebugSessionListener {
override fun sessionPaused() { childScope.cancel() }
override fun sessionResumed() { childScope.cancel() }
override fun sessionStopped() { childScope.cancel() }
}
session.addSessionListener(listener, childScope.asDisposable())
}
@@ -89,16 +89,22 @@ interface XDebugSessionProxy {
fun isSteppingSuspendContext(): Boolean
/**
* Computes execution stacks corresponding to all the live threads in the debug process and adds them to the provided container
* Computes execution stacks corresponding to all the live threads in the debug process and adds them to the provided [container]
* if suspendContext is available.
*/
fun computeExecutionStacks(container: XSuspendContext.XExecutionStackContainer)
/**
* Computes execution stacks corresponding to all the live threads in the debug process and adds them to the provided container.
* Uses [com.intellij.xdebugger.XDebugProcess.computeRunningExecutionStacks] on the backend and doesn't require suspendContext.
* Computes execution stacks corresponding to all the live threads in the debug process and adds them to the provided [container].
*
* - Uses [com.intellij.xdebugger.XDebugProcess.computeRunningExecutionStacks] to retrieve execution stacks.
* - Can provide execution stacks even if there is no active suspend context (i.e., when the process is running).
* - Supports execution stacks grouping. The provided [XSuspendContext.XExecutionStackGroupContainer] can accept
* [com.intellij.xdebugger.frame.XExecutionStackGroup] objects, allowing for a hierarchical display of execution stacks
* (e.g., grouped by thread group) in the UI.
*/
fun computeRunningExecutionStacks(container: XSuspendContext.XExecutionStackContainer)
fun computeRunningExecutionStacks(container: XSuspendContext.XExecutionStackGroupContainer)
fun createTabLayouter(): XDebugTabLayouter
fun addSessionListener(listener: XDebugSessionListener)
fun addSessionListener(listener: XDebugSessionListener, disposable: Disposable)
@@ -181,8 +181,8 @@ internal class MonolithSessionProxy(val session: XDebugSession) : XDebugSessionP
session.suspendContext?.computeExecutionStacks(container)
}
override fun computeRunningExecutionStacks(container: XSuspendContext.XExecutionStackContainer) {
session.debugProcess.computeRunningExecutionStacks(container)
override fun computeRunningExecutionStacks(container: XSuspendContext.XExecutionStackGroupContainer) {
session.debugProcess.computeRunningExecutionStacks(container, session.suspendContext)
}
override fun createTabLayouter(): XDebugTabLayouter = session.debugProcess.createTabLayouter()