[debugger] IDEA-338723 Fix suspending behavior while stepping in coroutines

Race conditions in continuation filtering in suspend-all mode force to switch to suspend-thread mode. But that behavior is different from suspend-all mode during regular stepping. To solve this problem, the workaround is implemented: found suspended thread calls suspend-all request (actually, track some method entering any thread). That next suspension resumes the original one-thead suspension and replaces the suspension to itself. So a user will just see "regular" suspend-all thread picture.

Note 1: Technically, the replaced suspended thread has another "primary" thread. It may lead to some problems.

Note 2: Suspend one-thread mode is working supported in coroutine stepping. But now it does not trigger thread switching (just notification is showing).

GitOrigin-RevId: 90d823e0ece4b6ce3a548ad984cd6e824dc516b4
This commit is contained in:
Alexey Merkulov
2024-02-05 19:41:01 +01:00
committed by intellij-monorepo-bot
parent d4f36c6437
commit 8b38a4f0d4
12 changed files with 189 additions and 26 deletions

View File

@@ -636,12 +636,17 @@ public class DebugProcessEvents extends DebugProcessImpl {
// Don't try to check breakpoint's condition or evaluate its log expression, // Don't try to check breakpoint's condition or evaluate its log expression,
// because these evaluations may lead to skipping of more important stepping events, // because these evaluations may lead to skipping of more important stepping events,
// see IDEA-336282. // see IDEA-336282.
if (!DebuggerSession.filterBreakpointsDuringSteppingUsingDebuggerEngine()) { boolean filterWasUsed = false;
boolean shouldIgnoreThreadFiltering = requestor == null || !requestor.shouldIgnoreThreadFiltering();
if (!DebuggerSession.filterBreakpointsDuringSteppingUsingDebuggerEngine() && shouldIgnoreThreadFiltering) {
LightOrRealThreadInfo filter = getRequestsManager().getFilterThread(); LightOrRealThreadInfo filter = getRequestsManager().getFilterThread();
if (filter != null && !filter.checkSameThread(thread, suspendContext)) { if (filter != null) {
notifySkippedBreakpoints(event, SkippedBreakpointReason.STEPPING); if (!filter.checkSameThread(thread, suspendContext)) {
suspendManager.voteResume(suspendContext); notifySkippedBreakpoints(event, SkippedBreakpointReason.STEPPING);
return; suspendManager.voteResume(suspendContext);
return;
}
filterWasUsed = true;
} }
} }
@@ -731,8 +736,18 @@ public class DebugProcessEvents extends DebugProcessImpl {
// // As resume() implicitly cleares the filter, the filter must be always applied _before_ any resume() action happens // // As resume() implicitly cleares the filter, the filter must be always applied _before_ any resume() action happens
// myBreakpointManager.applyThreadFilter(DebugProcessEvents.this, event.thread()); // myBreakpointManager.applyThreadFilter(DebugProcessEvents.this, event.thread());
//} //}
suspendManager.voteSuspend(suspendContext); if (filterWasUsed && suspendContext.getSuspendPolicy() == EventRequest.SUSPEND_EVENT_THREAD &&
showStatusText(DebugProcessEvents.this, event); requestor.needReplaceWithAllThreadSuspendContext()) {
// Do not vote to resume.
// Instead, create an auxiliary request to correctly stop all threads as soon as possible.
// [SuspendOtherThreadsRequestor] will resume this suspendContext when the request hits.
// Resume will be without voting.
SuspendOtherThreadsRequestor.enableRequest(mySession.getProcess(), suspendContext);
}
else {
suspendManager.voteSuspend(suspendContext);
showStatusText(DebugProcessEvents.this, event);
}
} }
} }
}); });

View File

@@ -1853,8 +1853,9 @@ public abstract class DebugProcessImpl extends UserDataHolderBase implements Deb
public RunToCursorCommand(SuspendContextImpl suspendContext, @NotNull XSourcePosition position, final boolean ignoreBreakpoints) { public RunToCursorCommand(SuspendContextImpl suspendContext, @NotNull XSourcePosition position, final boolean ignoreBreakpoints) {
super(suspendContext, null); super(suspendContext, null);
myIgnoreBreakpoints = ignoreBreakpoints; myIgnoreBreakpoints = ignoreBreakpoints;
myRunToCursorBreakpoint = boolean needReplaceWithAllThreadSuspendContext = suspendContext.getSuspendPolicy() == EventRequest.SUSPEND_ALL;
DebuggerManagerEx.getInstanceEx(myProject).getBreakpointManager().addRunToCursorBreakpoint(position, ignoreBreakpoints); myRunToCursorBreakpoint = DebuggerManagerEx.getInstanceEx(myProject).getBreakpointManager()
.addRunToCursorBreakpoint(position, ignoreBreakpoints, needReplaceWithAllThreadSuspendContext);
} }
@Override @Override
@@ -1985,7 +1986,8 @@ public abstract class DebugProcessImpl extends UserDataHolderBase implements Deb
} }
protected void applyThreadFilter(@Nullable LightOrRealThreadInfo threadInfo) { protected void applyThreadFilter(@Nullable LightOrRealThreadInfo threadInfo) {
if (getSuspendContext().getSuspendPolicy() == EventRequest.SUSPEND_ALL) { boolean isLightThread = threadInfo != null && threadInfo.getRealThread() == null;
if (isLightThread || getSuspendContext().getSuspendPolicy() == EventRequest.SUSPEND_ALL) {
// there could be explicit resume as a result of call to voteSuspend() // there could be explicit resume as a result of call to voteSuspend()
// e.g. when breakpoint was considered invalid, in that case the filter will be applied _after_ // e.g. when breakpoint was considered invalid, in that case the filter will be applied _after_
// resuming and all breakpoints in other threads will be ignored. // resuming and all breakpoints in other threads will be ignored.

View File

@@ -53,6 +53,8 @@ public abstract class SuspendContextImpl extends XSuspendContext implements Susp
private JavaExecutionStack myActiveExecutionStack; private JavaExecutionStack myActiveExecutionStack;
private @Nullable ThreadReferenceProxyImpl myAnotherThreadToFocus = null;
SuspendContextImpl(@NotNull DebugProcessImpl debugProcess, SuspendContextImpl(@NotNull DebugProcessImpl debugProcess,
@MagicConstant(flagsFromClass = EventRequest.class) int suspendPolicy, @MagicConstant(flagsFromClass = EventRequest.class) int suspendPolicy,
int eventVotes, int eventVotes,
@@ -334,4 +336,12 @@ public abstract class SuspendContextImpl extends XSuspendContext implements Susp
private static final Comparator<JavaExecutionStack> THREADS_SUSPEND_AND_NAME_COMPARATOR = private static final Comparator<JavaExecutionStack> THREADS_SUSPEND_AND_NAME_COMPARATOR =
Comparator.comparing(JavaExecutionStack::getThreadProxy, SUSPEND_FIRST_COMPARATOR).thenComparing(THREAD_NAME_COMPARATOR); Comparator.comparing(JavaExecutionStack::getThreadProxy, SUSPEND_FIRST_COMPARATOR).thenComparing(THREAD_NAME_COMPARATOR);
public @Nullable ThreadReferenceProxyImpl getAnotherThreadToFocus() {
return myAnotherThreadToFocus;
}
public void setAnotherThreadToFocus(@Nullable ThreadReferenceProxyImpl threadToFocus) {
myAnotherThreadToFocus = threadToFocus;
}
} }

View File

@@ -0,0 +1,98 @@
// Copyright 2000-2024 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.InstanceFilter;
import com.intellij.debugger.engine.events.SuspendContextCommandImpl;
import com.intellij.debugger.settings.DebuggerSettings;
import com.intellij.debugger.ui.breakpoints.FilteredRequestor;
import com.intellij.ui.classFilter.ClassFilter;
import com.sun.jdi.event.LocatableEvent;
import com.sun.jdi.request.EventRequest;
import org.jetbrains.annotations.NotNull;
class SuspendOtherThreadsRequestor implements FilteredRequestor {
private static final ClassFilter[] CLASS_EXCLUSION_FILTERS = {
new ClassFilter("java.*"),
new ClassFilter("jdk.*"),
new ClassFilter("sun.*"),
};
private final @NotNull DebugProcessImpl myProcess;
private final @NotNull SuspendContextImpl myThreadSuspendContext;
SuspendOtherThreadsRequestor(@NotNull DebugProcessImpl process, @NotNull SuspendContextImpl threadSuspendContext) {
myProcess = process;
myThreadSuspendContext = threadSuspendContext;
}
static void enableRequest(DebugProcessImpl process, @NotNull SuspendContextImpl threadSuspendContext) {
var requestor = new SuspendOtherThreadsRequestor(process, threadSuspendContext);
var request = process.getRequestsManager().createMethodEntryRequest(requestor);
request.setSuspendPolicy(EventRequest.SUSPEND_ALL);
request.setEnabled(true);
}
@Override
public boolean processLocatableEvent(@NotNull SuspendContextCommandImpl action, LocatableEvent event) {
myProcess.getRequestsManager().deleteRequest(this);
SuspendContextImpl suspendContext = action.getSuspendContext();
if (suspendContext == null) return false;
// Need to 'replace' the myThreadSuspendContext (single-thread suspend context passed filtering) with this one.
suspendContext.setAnotherThreadToFocus(myThreadSuspendContext.getThread());
// Note, myThreadSuspendContext is resuming without SuspendManager#voteSuspend.
// Look at the end of DebugProcessEvents#processLocatableEvent for more details.
myProcess.getSuspendManager().voteResume(myThreadSuspendContext);
return true;
}
@Override
public String getSuspendPolicy() {
return DebuggerSettings.SUSPEND_ALL;
}
@Override
public boolean isInstanceFiltersEnabled() {
return false;
}
@Override
public InstanceFilter[] getInstanceFilters() {
return InstanceFilter.EMPTY_ARRAY;
}
@Override
public boolean isCountFilterEnabled() {
return false;
}
@Override
public int getCountFilter() {
return 0;
}
@Override
public boolean isClassFiltersEnabled() {
return true;
}
@Override
public ClassFilter[] getClassFilters() {
return ClassFilter.EMPTY_ARRAY;
}
@Override
public ClassFilter[] getClassExclusionFilters() {
return CLASS_EXCLUSION_FILTERS;
}
@Override
public boolean shouldIgnoreThreadFiltering() {
return true;
}
}

View File

@@ -18,6 +18,14 @@ public interface LocatableEventRequestor extends Requestor {
*/ */
String getSuspendPolicy(); String getSuspendPolicy();
default boolean shouldIgnoreThreadFiltering() {
return false;
}
default boolean needReplaceWithAllThreadSuspendContext() {
return false;
}
class EventProcessingException extends Exception { class EventProcessingException extends Exception {
private final @NlsContexts.DialogTitle String myTitle; private final @NlsContexts.DialogTitle String myTitle;

View File

@@ -368,11 +368,15 @@ public final class DebuggerSession implements AbstractDebuggerSession {
public void resume() { public void resume() {
final SuspendContextImpl suspendContext = getSuspendContext(); final SuspendContextImpl suspendContext = getSuspendContext();
if (suspendContext != null) { if (suspendContext != null) {
clearSteppingThrough(); resumeSuspendContext(suspendContext);
resumeAction(myDebugProcess.createResumeCommand(suspendContext), Event.RESUME);
} }
} }
public void resumeSuspendContext(SuspendContextImpl suspendContext) {
clearSteppingThrough();
resumeAction(myDebugProcess.createResumeCommand(suspendContext), Event.RESUME);
}
public void resetIgnoreStepFiltersFlag() { public void resetIgnoreStepFiltersFlag() {
myIgnoreFiltersFrameCountThreshold = 0; myIgnoreFiltersFrameCountThreshold = 0;
} }
@@ -484,7 +488,8 @@ public final class DebuggerSession implements AbstractDebuggerSession {
public void paused(final SuspendContextImpl suspendContext) { public void paused(final SuspendContextImpl suspendContext) {
LOG.debug("paused"); LOG.debug("paused");
ThreadReferenceProxyImpl currentThread = suspendContext.getThread(); ThreadReferenceProxyImpl anotherThreadToFocus = suspendContext.getAnotherThreadToFocus();
ThreadReferenceProxyImpl currentThread = anotherThreadToFocus != null ? anotherThreadToFocus : suspendContext.getThread();
if (!shouldSetAsActiveContext(suspendContext)) { if (!shouldSetAsActiveContext(suspendContext)) {
notifyThreadsRefresh(); notifyThreadsRefresh();

View File

@@ -184,8 +184,8 @@ public class BreakpointManager {
} }
@Nullable @Nullable
public RunToCursorBreakpoint addRunToCursorBreakpoint(@NotNull XSourcePosition position, final boolean ignoreBreakpoints) { public RunToCursorBreakpoint addRunToCursorBreakpoint(@NotNull XSourcePosition position, boolean ignoreBreakpoints, boolean needReplaceWithAllThreadSuspendContext) {
return RunToCursorBreakpoint.create(myProject, position, ignoreBreakpoints); return RunToCursorBreakpoint.create(myProject, position, ignoreBreakpoints, needReplaceWithAllThreadSuspendContext);
} }
@Nullable @Nullable

View File

@@ -22,11 +22,13 @@ public class RunToCursorBreakpoint extends SyntheticLineBreakpoint implements St
private final boolean myRestoreBreakpoints; private final boolean myRestoreBreakpoints;
@NotNull @NotNull
protected final SourcePosition myCustomPosition; protected final SourcePosition myCustomPosition;
private final boolean myNeedReplaceWithAllThreadSuspendContext;
protected RunToCursorBreakpoint(@NotNull Project project, @NotNull SourcePosition pos, boolean restoreBreakpoints) { protected RunToCursorBreakpoint(@NotNull Project project, @NotNull SourcePosition pos, boolean restoreBreakpoints, boolean needReplaceWithAllThreadSuspendContext) {
super(project); super(project);
myCustomPosition = pos; myCustomPosition = pos;
myRestoreBreakpoints = restoreBreakpoints; myRestoreBreakpoints = restoreBreakpoints;
myNeedReplaceWithAllThreadSuspendContext = needReplaceWithAllThreadSuspendContext;
} }
@NotNull @NotNull
@@ -70,12 +72,15 @@ public class RunToCursorBreakpoint extends SyntheticLineBreakpoint implements St
} }
@Nullable @Nullable
protected static RunToCursorBreakpoint create(@NotNull Project project, @NotNull XSourcePosition position, boolean restoreBreakpoints) { protected static RunToCursorBreakpoint create(@NotNull Project project,
@NotNull XSourcePosition position,
boolean restoreBreakpoints,
boolean needReplaceWithAllThreadSuspendContext) {
PsiFile psiFile = PsiManager.getInstance(project).findFile(position.getFile()); PsiFile psiFile = PsiManager.getInstance(project).findFile(position.getFile());
if (psiFile == null) { if (psiFile == null) {
return null; return null;
} }
return new RunToCursorBreakpoint(project, SourcePosition.createFromOffset(psiFile, position.getOffset()), restoreBreakpoints); return new RunToCursorBreakpoint(project, SourcePosition.createFromOffset(psiFile, position.getOffset()), restoreBreakpoints, needReplaceWithAllThreadSuspendContext);
} }
@Override @Override
@@ -86,4 +91,9 @@ public class RunToCursorBreakpoint extends SyntheticLineBreakpoint implements St
public boolean track() { public boolean track() {
return false; return false;
} }
@Override
public boolean needReplaceWithAllThreadSuspendContext() {
return myNeedReplaceWithAllThreadSuspendContext;
}
} }

View File

@@ -26,7 +26,7 @@ public class StepIntoBreakpoint extends RunToCursorBreakpoint {
@Nullable private RequestHint myHint; @Nullable private RequestHint myHint;
protected StepIntoBreakpoint(@NotNull Project project, @NotNull SourcePosition pos, @NotNull BreakpointStepMethodFilter filter) { protected StepIntoBreakpoint(@NotNull Project project, @NotNull SourcePosition pos, @NotNull BreakpointStepMethodFilter filter) {
super(project, pos, false); super(project, pos, false, false);
myFilter = filter; myFilter = filter;
} }

View File

@@ -50,7 +50,6 @@ import com.intellij.testFramework.EdtTestUtil;
import com.intellij.testFramework.RunAll; import com.intellij.testFramework.RunAll;
import com.intellij.testFramework.UsefulTestCase; import com.intellij.testFramework.UsefulTestCase;
import com.intellij.util.ThrowableRunnable; import com.intellij.util.ThrowableRunnable;
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread;
import com.intellij.util.ui.EDT; import com.intellij.util.ui.EDT;
import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.UIUtil;
import com.intellij.xdebugger.*; import com.intellij.xdebugger.*;
@@ -453,7 +452,20 @@ public abstract class DebuggerTestCase extends ExecutionWithDebuggerToolsTestCas
} }
public DebuggerContextImpl createDebuggerContext(final SuspendContextImpl suspendContext) { public DebuggerContextImpl createDebuggerContext(final SuspendContextImpl suspendContext) {
return createDebuggerContext(suspendContext, suspendContext.getFrameProxy()); StackFrameProxyImpl proxy = getFrameProxy(suspendContext);
return createDebuggerContext(suspendContext, proxy);
}
protected static StackFrameProxyImpl getFrameProxy(@NotNull SuspendContextImpl suspendContext) {
if (suspendContext.getAnotherThreadToFocus() != null) {
try {
return suspendContext.getAnotherThreadToFocus().frame(0);
}
catch (EvaluateException e) {
throw new RuntimeException(e);
}
}
return suspendContext.getFrameProxy();
} }
protected void printLocation(SuspendContextImpl suspendContext) { protected void printLocation(SuspendContextImpl suspendContext) {

View File

@@ -31,6 +31,8 @@ import org.jetbrains.eval4j.Value
import org.jetbrains.eval4j.jdi.asValue import org.jetbrains.eval4j.jdi.asValue
import org.jetbrains.kotlin.descriptors.ModuleDescriptor import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.idea.KotlinFileType import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.base.test.InTextDirectivesUtils.findLinesWithPrefixesRemoved
import org.jetbrains.kotlin.idea.base.test.InTextDirectivesUtils.findStringWithPrefixes
import org.jetbrains.kotlin.idea.compilerPlugin.kotlinxSerialization.KotlinSerializationEnabledChecker import org.jetbrains.kotlin.idea.compilerPlugin.kotlinxSerialization.KotlinSerializationEnabledChecker
import org.jetbrains.kotlin.idea.debugger.core.CodeFragmentContextTuner import org.jetbrains.kotlin.idea.debugger.core.CodeFragmentContextTuner
import org.jetbrains.kotlin.idea.debugger.evaluate.DebugContextProvider import org.jetbrains.kotlin.idea.debugger.evaluate.DebugContextProvider
@@ -40,8 +42,6 @@ import org.jetbrains.kotlin.idea.debugger.test.util.FramePrinter
import org.jetbrains.kotlin.idea.debugger.test.util.FramePrinterDelegate import org.jetbrains.kotlin.idea.debugger.test.util.FramePrinterDelegate
import org.jetbrains.kotlin.idea.debugger.test.util.KotlinOutputChecker import org.jetbrains.kotlin.idea.debugger.test.util.KotlinOutputChecker
import org.jetbrains.kotlin.idea.debugger.test.util.SteppingInstruction import org.jetbrains.kotlin.idea.debugger.test.util.SteppingInstruction
import org.jetbrains.kotlin.idea.base.test.InTextDirectivesUtils.findLinesWithPrefixesRemoved
import org.jetbrains.kotlin.idea.base.test.InTextDirectivesUtils.findStringWithPrefixes
import org.jetbrains.kotlin.idea.test.KotlinBaseTest import org.jetbrains.kotlin.idea.test.KotlinBaseTest
import java.io.File import java.io.File
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@@ -210,7 +210,7 @@ abstract class AbstractIrKotlinEvaluateExpressionTest : KotlinDescriptorTestCase
val sourcePosition = ContextUtil.getSourcePosition(suspendContext) val sourcePosition = ContextUtil.getSourcePosition(suspendContext)
// Default test debuggerContext doesn't provide a valid stackFrame so we have to create one more for evaluation purposes. // Default test debuggerContext doesn't provide a valid stackFrame so we have to create one more for evaluation purposes.
val frameProxy = suspendContext.frameProxy val frameProxy = getFrameProxy(suspendContext)
val threadProxy = frameProxy?.threadProxy() val threadProxy = frameProxy?.threadProxy()
val debuggerContext = createDebuggerContext(myDebuggerSession, suspendContext, threadProxy, frameProxy) val debuggerContext = createDebuggerContext(myDebuggerSession, suspendContext, threadProxy, frameProxy)
debuggerContext.initCaches() debuggerContext.initCaches()

View File

@@ -91,7 +91,7 @@ abstract class KotlinDescriptorTestCaseWithStepping : KotlinDescriptorTestCase()
private fun SuspendContextImpl.getKotlinStackFrames(): List<KotlinStackFrame> { private fun SuspendContextImpl.getKotlinStackFrames(): List<KotlinStackFrame> {
if (myInProgress) { if (myInProgress) {
val proxy = frameProxy ?: return emptyList() val proxy = getFrameProxy(this) ?: return emptyList()
return KotlinPositionManager(debugProcess) return KotlinPositionManager(debugProcess)
.createStackFrames(StackFrameDescriptorImpl(proxy, MethodsTracker())) .createStackFrames(StackFrameDescriptorImpl(proxy, MethodsTracker()))
.filterIsInstance<KotlinStackFrame>() .filterIsInstance<KotlinStackFrame>()
@@ -251,7 +251,10 @@ abstract class KotlinDescriptorTestCaseWithStepping : KotlinDescriptorTestCase()
return@runReadAction println("Context thread is null", ProcessOutputTypes.SYSTEM) return@runReadAction println("Context thread is null", ProcessOutputTypes.SYSTEM)
} }
val sourcePosition = PositionUtil.getSourcePosition(this) val sourcePosition = if (anotherThreadToFocus != null)
debugProcess.positionManager.getSourcePosition(getFrameProxy(this).location())
else
PositionUtil.getSourcePosition(this)
println(sourcePosition?.render() ?: "null", ProcessOutputTypes.SYSTEM) println(sourcePosition?.render() ?: "null", ProcessOutputTypes.SYSTEM)
extraPrintContext(this) extraPrintContext(this)
} }