[debugger] LSP-423: Migrate execution stack calculation from flow to suspend function

Current implementations of execution stack computations immediately return the list of execution stacks with last = true, so let's just wait for all coming execution stacks instead of getting them incrementally.
This will reduce code complexity around flows and its lifetimes

GitOrigin-RevId: 6b15710796939c017b9e8b848510669193286fb0
This commit is contained in:
Nikolay Rykunov
2026-01-15 16:24:25 +00:00
committed by intellij-monorepo-bot
parent 3d04daabc9
commit 9cf0c2e699
5 changed files with 137 additions and 79 deletions
@@ -1,4 +1,4 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2026 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.backend
import com.intellij.ide.rpc.FrontendDocumentId
@@ -14,11 +14,9 @@ import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.event.DocumentListener
import com.intellij.openapi.project.IndexNotReadyException
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsContexts
import com.intellij.platform.debugger.impl.rpc.*
import com.intellij.platform.project.ProjectId
import com.intellij.platform.project.findProject
import com.intellij.platform.util.coroutines.attachAsChildTo
import com.intellij.platform.util.coroutines.childScope
import com.intellij.ui.FileColorManager
import com.intellij.util.ThreeState
@@ -29,12 +27,10 @@ import com.intellij.xdebugger.evaluation.EvaluationMode
import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider
import com.intellij.xdebugger.frame.XExecutionStack
import com.intellij.xdebugger.frame.XStackFrame
import com.intellij.xdebugger.frame.XSuspendContext
import com.intellij.xdebugger.impl.XDebugSessionImpl
import com.intellij.xdebugger.impl.XSourceKind
import com.intellij.xdebugger.impl.XSteppingSuspendContext
import com.intellij.xdebugger.impl.frame.XStackFrameWithCustomBackgroundColor
import com.intellij.xdebugger.impl.frame.XStackFrameWithSeparatorAbove
import com.intellij.xdebugger.impl.frame.*
import com.intellij.xdebugger.impl.rpc.models.findValue
import com.intellij.xdebugger.impl.rpc.models.getOrStoreGlobally
import com.intellij.xdebugger.impl.rpc.models.storeGlobally
@@ -44,15 +40,10 @@ import com.intellij.xdebugger.stepping.ForceSmartStepIntoSource
import com.intellij.xdebugger.stepping.XSmartStepIntoHandler
import com.intellij.xdebugger.stepping.XSmartStepIntoVariant
import fleet.rpc.core.toRpc
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.future.asDeferred
import kotlinx.coroutines.withContext
import org.jetbrains.concurrency.Promise
import org.jetbrains.concurrency.await
import org.jetbrains.concurrency.rejectedPromise
@@ -200,57 +191,39 @@ internal class BackendXDebugSessionApi : XDebugSessionApi {
}
}
override suspend fun computeRunningExecutionStacks(sessionId: XDebugSessionId): Flow<XExecutionStacksEvent> {
val session = sessionId.findValue() ?: return emptyFlow()
val scope = session.coroutineScope.childScopeCancelledOnSessionEvents("RunningExecutionStacksScope", session)
return createExecutionStacksEventFlow(session, scope) { container ->
session.debugProcess.computeRunningExecutionStacks(container)
override suspend fun computeRunningExecutionStacks(sessionId: XDebugSessionId): TimeoutSafeResult<XExecutionStacksResult> {
val session = sessionId.findValue()
?: return CompletableDeferred(value = XExecutionStacksResult.ExecutionStacks(listOf()))
return computeExecutionStacksImpl(session.coroutineScope, session) {
session.debugProcess.computeRunningExecutionStacksSuspend()
}
}
override suspend fun computeExecutionStacks(suspendContextId: XSuspendContextId): Flow<XExecutionStacksEvent> {
val suspendContextModel = suspendContextId.findValue() ?: return emptyFlow()
override suspend fun computeExecutionStacks(suspendContextId: XSuspendContextId): TimeoutSafeResult<XExecutionStacksResult> {
val suspendContextModel = suspendContextId.findValue()
?: return CompletableDeferred(value = XExecutionStacksResult.ExecutionStacks(listOf()))
val session = suspendContextModel.session
return createExecutionStacksEventFlow(session, suspendContextModel.coroutineScope) { container ->
suspendContextModel.suspendContext.computeExecutionStacks(container)
return computeExecutionStacksImpl(suspendContextModel.coroutineScope, session) {
suspendContextModel.suspendContext.computeExecutionStacksSuspend()
}
}
private fun createExecutionStacksEventFlow(
session: XDebugSessionImpl,
private fun computeExecutionStacksImpl(
scope: CoroutineScope,
computeExecutionStacks: (XSuspendContext.XExecutionStackContainer) -> Unit
) : Flow<XExecutionStacksEvent> {
return channelFlow {
attachAsChildTo(scope)
val container = object : XSuspendContext.XExecutionStackContainer {
@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(XExecutionStacksEvent.NewExecutionStacks(stacks, last))
if (last) {
this@channelFlow.close()
}
}
override fun errorOccurred(errorMessage: @NlsContexts.DialogMessage String) {
trySend(XExecutionStacksEvent.ErrorOccurred(errorMessage))
session: XDebugSessionImpl,
computeExecutionStacks: suspend () -> List<XExecutionStack>,
): TimeoutSafeResult<XExecutionStacksResult> {
return scope.async {
try {
val stackDtos = computeExecutionStacks().map { stack ->
stack.toRpc(scope, session)
}
XExecutionStacksResult.ExecutionStacks(stackDtos)
}
computeExecutionStacks(container)
awaitClose {
container.obsolete = true
catch (e: XExecutionStacksComputationException) {
XExecutionStacksResult.ErrorOccurred(e.message ?: "Error occurred while computing execution stacks")
}
}.buffer(Channel.UNLIMITED)
}
}
private fun CoroutineScope.childScopeCancelledOnSessionEvents(name: String, session: XDebugSessionImpl): CoroutineScope =
@@ -1,4 +1,4 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2026 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.frontend
import com.intellij.execution.RunContentDescriptorIdImpl
@@ -437,7 +437,8 @@ class FrontendXDebuggerSession private constructor(
coroutineScope.launch {
XDebugSessionApi.getInstance()
.computeRunningExecutionStacks(id)
.collectExecutionStackEvents(project, coroutineScope, container)
.await()
.handleResult(project, coroutineScope, container)
}
}
@@ -1,11 +1,11 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2026 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.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.XDebugSessionApi
import com.intellij.platform.debugger.impl.rpc.XExecutionStacksEvent
import com.intellij.platform.debugger.impl.rpc.XExecutionStacksResult
import com.intellij.platform.debugger.impl.rpc.XStackFrameDto
import com.intellij.platform.debugger.impl.rpc.XSuspendContextDto
import com.intellij.platform.util.coroutines.childScope
@@ -14,7 +14,6 @@ import com.intellij.xdebugger.frame.XSuspendContext
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
internal class FrontendXSuspendContext(
@@ -41,7 +40,8 @@ internal class FrontendXSuspendContext(
lifetimeScope.launch {
XDebugSessionApi.getInstance()
.computeExecutionStacks(id)
.collectExecutionStackEvents(project, lifetimeScope, container)
.await()
.handleResult(project, lifetimeScope, container)
}
}
@@ -54,24 +54,23 @@ internal class FrontendXSuspendContext(
}
}
internal suspend fun Flow<XExecutionStacksEvent>.collectExecutionStackEvents(
internal fun XExecutionStacksResult.handleResult(
project: Project,
coroutineScope: CoroutineScope,
container: XSuspendContext.XExecutionStackContainer
container: XSuspendContext.XExecutionStackContainer,
) {
collect { executionStackEvent ->
when (executionStackEvent) {
is XExecutionStacksEvent.ErrorOccurred -> {
container.errorOccurred(executionStackEvent.errorMessage)
}
is XExecutionStacksEvent.NewExecutionStacks -> {
// 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.
// But it requires further investigation.
val feStacks = executionStackEvent.stacks.map { FrontendXExecutionStack(it, project, coroutineScope) }
container.addExecutionStack(feStacks, executionStackEvent.last)
}
val executionStackEvent = this
when (executionStackEvent) {
is XExecutionStacksResult.ErrorOccurred -> {
container.errorOccurred(executionStackEvent.errorMessage)
}
is XExecutionStacksResult.ExecutionStacks -> {
// 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.
// But it requires further investigation.
val feStacks = executionStackEvent.stacks.map { FrontendXExecutionStack(it, project, coroutineScope) }
container.addExecutionStack(feStacks, true)
}
}
}
@@ -1,4 +1,4 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2026 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.rpc
import com.intellij.execution.RunContentDescriptorIdImpl
@@ -58,9 +58,9 @@ interface XDebugSessionApi : RemoteApi<Unit> {
suspend fun setCurrentStackFrame(sessionId: XDebugSessionId, executionStackId: XExecutionStackId, frameId: XStackFrameId, isTopFrame: Boolean, changedByUser: Boolean = false)
suspend fun computeExecutionStacks(suspendContextId: XSuspendContextId): Flow<XExecutionStacksEvent>
suspend fun computeExecutionStacks(suspendContextId: XSuspendContextId): TimeoutSafeResult<XExecutionStacksResult>
suspend fun computeRunningExecutionStacks(sessionId: XDebugSessionId): Flow<XExecutionStacksEvent>
suspend fun computeRunningExecutionStacks(sessionId: XDebugSessionId): TimeoutSafeResult<XExecutionStacksResult>
suspend fun muteBreakpoints(sessionDataId: XDebugSessionDataId, muted: Boolean)
@@ -98,12 +98,12 @@ data class XDebugSessionDto(
@ApiStatus.Internal
@Serializable
sealed interface XExecutionStacksEvent {
sealed interface XExecutionStacksResult {
@Serializable
data class NewExecutionStacks(val stacks: List<XExecutionStackDto>, val last: Boolean) : XExecutionStacksEvent
data class ExecutionStacks(val stacks: List<XExecutionStackDto>) : XExecutionStacksResult
@Serializable
data class ErrorOccurred(val errorMessage: @NlsContexts.DialogMessage String) : XExecutionStacksEvent
data class ErrorOccurred(val errorMessage: @NlsContexts.DialogMessage String) : XExecutionStacksResult
}
@ApiStatus.Internal
@@ -0,0 +1,85 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.xdebugger.impl.frame
import com.intellij.openapi.util.NlsContexts
import com.intellij.xdebugger.XDebugProcess
import com.intellij.xdebugger.frame.XExecutionStack
import com.intellij.xdebugger.frame.XSuspendContext
import kotlinx.coroutines.CompletableDeferred
import org.jetbrains.annotations.ApiStatus
/**
* Computes the list of running execution stacks for this debug process.
*
* This is a suspend wrapper around [XDebugProcess.computeRunningExecutionStacks] that converts
* the callback-based API to a coroutine-based one.
*
* @return the list of currently running [XExecutionStack]s
* @throws XExecutionStacksComputationException if an error occurs during computation
*/
@ApiStatus.Internal
suspend fun XDebugProcess.computeRunningExecutionStacksSuspend(): List<XExecutionStack> {
return computeExecutionStacksImpl {
computeRunningExecutionStacks(it)
}
}
/**
* Computes all execution stacks for this suspend context.
*
* This is a suspend wrapper around [XSuspendContext.computeExecutionStacks] that converts
* the callback-based API to a coroutine-based one.
*
* @return the list of all [XExecutionStack]s in this suspend context
* @throws XExecutionStacksComputationException if an error occurs during computation
*/
@ApiStatus.Internal
suspend fun XSuspendContext.computeExecutionStacksSuspend(): List<XExecutionStack> {
return computeExecutionStacksImpl { container ->
computeExecutionStacks(container)
}
}
/**
* Exception thrown when an error occurs during the computation of execution stacks.
*
* This exception wraps error messages reported via [com.intellij.xdebugger.frame.XSuspendContext.XExecutionStackContainer.errorOccurred].
*
* @param message the error message describing what went wrong during computation
*/
@ApiStatus.Internal
class XExecutionStacksComputationException(message: String) : Exception(message)
private suspend fun computeExecutionStacksImpl(
computeExecutionStacks: (XSuspendContext.XExecutionStackContainer) -> Unit,
): List<XExecutionStack> {
val result = mutableListOf<XExecutionStack>()
val computationCompleted = CompletableDeferred<Unit>()
var obsolete = false
val container = object : XSuspendContext.XExecutionStackContainer {
override fun isObsolete(): Boolean {
return obsolete
}
override fun addExecutionStack(executionStacks: List<XExecutionStack>, last: Boolean) {
result.addAll(executionStacks)
if (last) {
computationCompleted.complete(Unit)
}
}
override fun errorOccurred(errorMessage: @NlsContexts.DialogMessage String) {
computationCompleted.completeExceptionally(XExecutionStacksComputationException(errorMessage))
}
}
try {
computeExecutionStacks(container)
computationCompleted.await()
}
finally {
obsolete = true
}
return result
}