Skip to content

Commit 1b67cf1

Browse files
committed
[GR-8251] Added DebuggerSession.disposeStep(Thread) and the lifecycle of steps is now independent of breakpoint hits.
PullRequest: graal/21980
2 parents 226981e + e018b9d commit 1b67cf1

File tree

10 files changed

+540
-128
lines changed

10 files changed

+540
-128
lines changed

espresso/src/com.oracle.truffle.espresso.jdwp/src/com/oracle/truffle/espresso/jdwp/impl/DebuggerController.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,10 @@ public void clearBreakpoints() {
384384
}
385385

386386
public void clearStepCommand(StepInfo stepInfo) {
387-
commandRequestIds.remove(stepInfo.getGuestThread());
387+
Object guestThread = stepInfo.getGuestThread();
388+
Thread hostThread = getContext().asHostThread(guestThread);
389+
debuggerSession.disposeStepping(hostThread);
390+
commandRequestIds.remove(guestThread);
388391
}
389392

390393
public boolean popFrames(Object guestThread, CallFrame frameToPop, int packetId) {
@@ -965,9 +968,8 @@ public void onSuspend(SuspendedEvent event) {
965968

966969
result = checkForBreakpoints(event, jobs, suspendedInfo, currentThread, callFrames);
967970
if (!result.breakpointHit) {
968-
// no breakpoint
971+
// no breakpoint, still have a pending step
969972
commandRequestIds.put(currentThread, steppingInfo);
970-
continueStepping(event, steppingInfo);
971973
}
972974
}
973975
} else {

truffle/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ This changelog summarizes major changes between Truffle versions relevant to lan
1919

2020
* GR-68916: Added `TruffleString.MaterializeLazySubstringNode`. Use this node to free any unnecessary memory held by lazy substrings.
2121
* GR-68916: Added `TruffleString.MaterializeSubstringNode`. Use this node to free any unnecessary memory held by lazy substrings.
22+
* GR-8251: Added `DebuggerSession.disposeStepping(Thread)` to the debugger API to allow disposal of any pending step on a thread.
23+
* GR-8251: Pending steps are no longer removed when no debugging action is prepared on a `SuspendedEvent`. In practice, this means that the lifecycle of steps is now independent of breakpoint hits.
24+
* GR-8251: `DebuggerSession.resumeThread(Thread)` no longer cancels ongoing step operations. Stepping is now independent of other debugger actions to enhance flexibility.
2225

2326
## Version 25.0
2427
* GR-31495 Added ability to specify language and instrument specific options using `Source.Builder.option(String, String)`. Languages may describe available source options by implementing `TruffleLanguage.getSourceOptionDescriptors()` and `TruffleInstrument.getSourceOptionDescriptors()` respectively.

truffle/src/com.oracle.truffle.api.debug.test/src/com/oracle/truffle/api/debug/test/AbstractDebugTest.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2016, 2022, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -258,7 +258,9 @@ protected void checkStack(DebugStackFrame frame, String... expectedFrame) {
258258
String expectedValue = expectedFrame[i + 1];
259259
DebugValue value = values.get(expectedIdentifier);
260260
Assert.assertNotNull("Identifier " + expectedIdentifier + " not found.", value);
261-
Assert.assertEquals(expectedValue, value.toDisplayString());
261+
if (expectedValue != null) {
262+
Assert.assertEquals(expectedValue, value.toDisplayString());
263+
}
262264
}
263265
}
264266

truffle/src/com.oracle.truffle.api.debug.test/src/com/oracle/truffle/api/debug/test/DebuggerSessionTest.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,8 +601,11 @@ public void testResumeThread1() {
601601
expectSuspended((SuspendedEvent event) -> {
602602
checkState(event, 2, true, "STATEMENT").prepareStepOver(1);
603603
});
604-
// resume events are ignored by stepping
605604
session.resume(getEvalThread());
605+
// Step was prepared, resume has no effect on stepping
606+
expectSuspended((SuspendedEvent event) -> {
607+
checkState(event, 3, true, "STATEMENT").prepareContinue();
608+
});
606609
expectDone();
607610
}
608611
}
@@ -625,6 +628,10 @@ public void testResumeThread2() {
625628
checkState(event, 2, true, "STATEMENT").prepareStepOver(1);
626629
});
627630
session.resume(getEvalThread());
631+
// Step was prepared, resume has no effect on stepping
632+
expectSuspended((SuspendedEvent event) -> {
633+
checkState(event, 3, true, "STATEMENT").prepareContinue();
634+
});
628635
expectDone();
629636
}
630637
}

truffle/src/com.oracle.truffle.api.debug.test/src/com/oracle/truffle/api/debug/test/DoubleHaltTest.java

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -41,7 +41,9 @@
4141
package com.oracle.truffle.api.debug.test;
4242

4343
import static org.junit.Assert.assertEquals;
44+
import static org.junit.Assert.assertFalse;
4445
import static org.junit.Assert.assertSame;
46+
import static org.junit.Assert.assertTrue;
4547

4648
import org.junit.Test;
4749

@@ -78,27 +80,31 @@ public void testBreakpointStepping() throws Throwable {
7880
checkState(event, 2, true, "STATEMENT");
7981
assertEquals(1, event.getBreakpoints().size());
8082
assertSame(breakpoint2, event.getBreakpoints().iterator().next());
83+
assertFalse(event.isStep());
8184
event.prepareStepInto(1);
8285
});
8386

8487
expectSuspended((SuspendedEvent event) -> {
8588
checkState(event, 3, true, "STATEMENT");
8689
assertEquals(1, event.getBreakpoints().size());
8790
assertSame(breakpoint3, event.getBreakpoints().iterator().next());
91+
assertTrue(event.isStep());
8892
event.prepareStepOver(2);
8993
});
9094

9195
expectSuspended((SuspendedEvent event) -> {
9296
checkState(event, 5, true, "STATEMENT");
9397
assertEquals(1, event.getBreakpoints().size());
9498
assertSame(breakpoint5, event.getBreakpoints().iterator().next());
99+
assertTrue(event.isStep());
95100
event.prepareStepInto(2);
96101
});
97102

98103
expectSuspended((SuspendedEvent event) -> {
99104
checkState(event, 6, true, "STATEMENT");
100105
assertEquals(1, event.getBreakpoints().size());
101106
assertSame(breakpoint6, event.getBreakpoints().iterator().next());
107+
assertFalse(event.isStep());
102108
event.prepareContinue();
103109
});
104110

@@ -111,6 +117,137 @@ public void testBreakpointStepping() throws Throwable {
111117
}
112118
}
113119

120+
@Test
121+
public void testBreakpointAndStep() throws Throwable {
122+
Source testSource = testSource("ROOT(\n" +
123+
" DEFINE(foo,\n" +
124+
" ROOT(\n" +
125+
" STATEMENT,\n" +
126+
" STATEMENT)\n" +
127+
" ),\n" +
128+
" STATEMENT,\n" +
129+
" CALL(foo),\n" +
130+
" STATEMENT\n" +
131+
")\n");
132+
133+
// Breakpoint inside `foo` breaks a step over `CALL(foo)`.
134+
// Test that the step is finished after we leave the breakpoint
135+
try (DebuggerSession session = startSession()) {
136+
Breakpoint breakpoint = session.install(Breakpoint.newBuilder(getSourceImpl(testSource)).lineIs(4).build());
137+
138+
session.suspendNextExecution();
139+
startEval(testSource);
140+
141+
expectSuspended((SuspendedEvent event) -> {
142+
checkState(event, 7, true, "STATEMENT");
143+
assertEquals(0, event.getBreakpoints().size());
144+
assertFalse(event.isStep());
145+
event.prepareStepOver(1);
146+
});
147+
148+
expectSuspended((SuspendedEvent event) -> {
149+
checkState(event, 4, true, "STATEMENT");
150+
assertEquals(1, event.getBreakpoints().size());
151+
assertSame(breakpoint, event.getBreakpoints().iterator().next());
152+
assertFalse(event.isStep());
153+
});
154+
155+
expectSuspended((SuspendedEvent event) -> {
156+
checkState(event, 9, true, "STATEMENT");
157+
assertEquals(0, event.getBreakpoints().size());
158+
assertTrue(event.isStep());
159+
event.prepareContinue();
160+
});
161+
}
162+
}
163+
164+
@Test
165+
public void testBreakpointAndStep2() throws Throwable {
166+
Source testSource = testSource("ROOT(\n" +
167+
" DEFINE(foo,\n" +
168+
" ROOT(\n" +
169+
" STATEMENT,\n" +
170+
" STATEMENT)\n" +
171+
" ),\n" +
172+
" STATEMENT,\n" +
173+
" CALL(foo),\n" +
174+
" STATEMENT\n" +
175+
")\n");
176+
177+
// Breakpoint inside `foo` breaks a step over `CALL(foo)`.
178+
// We explicitly prepare continue and that cancels the step
179+
try (DebuggerSession session = startSession()) {
180+
Breakpoint breakpoint = session.install(Breakpoint.newBuilder(getSourceImpl(testSource)).lineIs(4).build());
181+
182+
session.suspendNextExecution();
183+
startEval(testSource);
184+
185+
expectSuspended((SuspendedEvent event) -> {
186+
checkState(event, 7, true, "STATEMENT");
187+
assertEquals(0, event.getBreakpoints().size());
188+
assertFalse(event.isStep());
189+
event.prepareStepOver(1);
190+
});
191+
192+
expectSuspended((SuspendedEvent event) -> {
193+
checkState(event, 4, true, "STATEMENT");
194+
assertEquals(1, event.getBreakpoints().size());
195+
assertSame(breakpoint, event.getBreakpoints().iterator().next());
196+
assertFalse(event.isStep());
197+
event.prepareContinue();
198+
});
199+
200+
expectDone();
201+
}
202+
}
203+
204+
@Test
205+
public void testBreakpointAndStep3() throws Throwable {
206+
Source testSource = testSource("ROOT(\n" +
207+
" DEFINE(foo,\n" +
208+
" ROOT(\n" +
209+
" STATEMENT,\n" +
210+
" STATEMENT)\n" +
211+
" ),\n" +
212+
" STATEMENT,\n" +
213+
" CALL(foo),\n" +
214+
" STATEMENT\n" +
215+
")\n");
216+
217+
// Breakpoint inside `foo` breaks a step over `CALL(foo)`.
218+
// We explicitly prepare a new step and that cancels the original one
219+
try (DebuggerSession session = startSession()) {
220+
Breakpoint breakpoint = session.install(Breakpoint.newBuilder(getSourceImpl(testSource)).lineIs(4).build());
221+
222+
session.suspendNextExecution();
223+
startEval(testSource);
224+
225+
expectSuspended((SuspendedEvent event) -> {
226+
checkState(event, 7, true, "STATEMENT");
227+
assertEquals(0, event.getBreakpoints().size());
228+
assertFalse(event.isStep());
229+
event.prepareStepOver(1);
230+
});
231+
232+
expectSuspended((SuspendedEvent event) -> {
233+
checkState(event, 4, true, "STATEMENT");
234+
assertEquals(1, event.getBreakpoints().size());
235+
assertSame(breakpoint, event.getBreakpoints().iterator().next());
236+
assertFalse(event.isStep());
237+
event.prepareStepOver(1);
238+
});
239+
240+
expectSuspended((SuspendedEvent event) -> {
241+
checkState(event, 5, true, "STATEMENT");
242+
assertEquals(0, event.getBreakpoints().size());
243+
assertTrue(event.isStep());
244+
event.prepareContinue();
245+
});
246+
247+
expectDone();
248+
}
249+
}
250+
114251
@Test
115252
public void testCallLoopStepInto() throws Throwable {
116253
Source testSource = testSource("ROOT(\n" +

truffle/src/com.oracle.truffle.api.debug.test/src/com/oracle/truffle/api/debug/test/StepTest.java

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2016, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -1361,6 +1361,106 @@ public void testSteppingDisabledNestedWithBreakpoint() throws Exception {
13611361
}
13621362
}
13631363

1364+
@Test
1365+
public void testStepDispose() throws Exception {
1366+
final Source source = testSource("ROOT(\n" +
1367+
" STATEMENT\n" +
1368+
")\n");
1369+
try (DebuggerSession session = startSession()) {
1370+
session.suspendNextExecution();
1371+
startEval(source);
1372+
expectSuspended((SuspendedEvent event) -> {
1373+
checkState(event, 2, true, "STATEMENT").prepareStepInto(1);
1374+
});
1375+
expectDone();
1376+
startEval(source);
1377+
expectSuspended((SuspendedEvent event) -> {
1378+
checkState(event, 2, true, "STATEMENT").prepareStepInto(1);
1379+
});
1380+
expectDone();
1381+
startExecute(c -> {
1382+
session.disposeStepping(Thread.currentThread());
1383+
return c.asValue(null);
1384+
});
1385+
expectDone();
1386+
startEval(source);
1387+
expectDone();
1388+
}
1389+
}
1390+
1391+
@Test
1392+
public void testStepDispose2() throws Exception {
1393+
final Source source = testSource("ROOT(\n" +
1394+
" STATEMENT\n" +
1395+
")\n");
1396+
try (DebuggerSession session = startSession()) {
1397+
session.suspendNextExecution();
1398+
startEval(source);
1399+
expectSuspended((SuspendedEvent event) -> {
1400+
checkState(event, 2, true, "STATEMENT").prepareStepInto(1);
1401+
});
1402+
expectDone();
1403+
startEval(source);
1404+
expectSuspended((SuspendedEvent event) -> {
1405+
checkState(event, 2, true, "STATEMENT").prepareStepInto(1);
1406+
});
1407+
expectDone();
1408+
startExecute(c -> {
1409+
// Stepping can be disposed from any thread
1410+
Thread steppingThread = Thread.currentThread();
1411+
Thread t = new Thread(() -> session.disposeStepping(steppingThread));
1412+
t.start();
1413+
try {
1414+
t.join();
1415+
} catch (InterruptedException e) {
1416+
fail("Interrupted");
1417+
}
1418+
return c.asValue(null);
1419+
});
1420+
expectDone();
1421+
startEval(source);
1422+
expectDone();
1423+
}
1424+
}
1425+
1426+
@Test
1427+
public void testStepDispose3() throws Exception {
1428+
final Source source = testSource("ROOT(\n" +
1429+
" STATEMENT\n" +
1430+
")\n");
1431+
try (DebuggerSession session = startSession()) {
1432+
session.suspendNextExecution();
1433+
startEval(source);
1434+
expectSuspended((SuspendedEvent event) -> {
1435+
checkState(event, 2, true, "STATEMENT").prepareStepInto(1);
1436+
});
1437+
expectDone();
1438+
startEval(source);
1439+
expectSuspended((SuspendedEvent event) -> {
1440+
checkState(event, 2, true, "STATEMENT").prepareStepInto(1);
1441+
});
1442+
expectDone();
1443+
startExecute(c -> {
1444+
// Stepping disabled on a different thread has no effect
1445+
Thread t = new Thread(() -> session.disposeStepping(Thread.currentThread()));
1446+
t.start();
1447+
try {
1448+
t.join();
1449+
} catch (InterruptedException e) {
1450+
fail("Interrupted");
1451+
}
1452+
return c.asValue(null);
1453+
});
1454+
expectDone();
1455+
startEval(source);
1456+
// Stepping not disposed
1457+
expectSuspended((SuspendedEvent event) -> {
1458+
checkState(event, 2, true, "STATEMENT").prepareContinue();
1459+
});
1460+
expectDone();
1461+
}
1462+
}
1463+
13641464
private static void runAndCheckForErrors(Thread thread) throws InterruptedException {
13651465
joinWithErrorCheck(thread, runWithErrorCheck(thread));
13661466
}

0 commit comments

Comments
 (0)