Skip to content

Commit c2a4f8e

Browse files
DanTupCommit Queue
authored andcommitted
[dds/dap] Ensure breakpoint modifications do not drop resolution events
When adding new breakpoints, DAP clients send a full new set of breakpoints which results in us removing and re-adding all breakpoints (for a file). When we re-add a breakpoint that already existed, we would sometimes get the BreakpointAdded event _before_ the addBreakpointWithScriptUri request completed. We couldn't process the event because without the original request completing we could not map the VM breakpoint back to the clients breakpoint to generate the event. This could result in resolution events being lost, and breakpoints becoming unresolved when you modified them (depending on timing). This change collects queues and replays any BreakpointAdded/BreakpointResolved events that arrived when the breakpoint ID is unknown, and when `addBreakpointWithScriptUri` completes, it immediately processes any items in this queue. Fixes Dart-Code/Dart-Code#4599 Change-Id: I11daaf99b786ab94f1cc93f9fd38a4f1e241320f Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/310620 Commit-Queue: Ben Konyi <[email protected]> Reviewed-by: Ben Konyi <[email protected]>
1 parent 7f2e70e commit c2a4f8e

File tree

5 files changed

+129
-7
lines changed

5 files changed

+129
-7
lines changed

pkg/dds/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
- [DAP] `runInTerminal` requests are now sent after first responding to the `launchRequest`.
33
- [DAP] Skipped tests are now marked with `!` instead of `` in `Output` events.
44
- [DAP] Implemented `pause` request.
5+
- [DAP] Fixed an issue that could leave breakpoints unresolved when adding/removing other breakpoints in a file.
56
- Fixed a bug that was preventing clients from receiving `IsolateReload` events
67
(see https://dartbug.com/49491).
78

pkg/dds/lib/src/dap/isolate_manager.dart

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,20 @@ class IsolateManager {
7171

7272
/// Tracks client breakpoints by the ID assigned by the VM so we can look up
7373
/// conditions/logpoints when hitting breakpoints.
74+
///
75+
/// When an item is added to this map, any pending events in
76+
/// [_pendingBreakpointEvents] MUST be processed immediately.
7477
final Map<String, ClientBreakpoint> _clientBreakpointsByVmId = {};
7578

79+
/// Tracks breakpoints for which we have received `BreakpointAdded` or
80+
/// `BreakpointResolved` events, but whose `addBreakpointWithScriptUri`
81+
/// requests have not yet completed.
82+
///
83+
/// We cannot process these events until after we get the breakpoint ID
84+
/// back from the VM. This queue MUST be processed immediately any time a
85+
/// breakpoint is added to [_clientBreakpointsByVmId].
86+
final Map<String, PendingBreakpointActions> _pendingBreakpointEvents = {};
87+
7688
/// Tracks breakpoints created in the VM so they can be removed when the
7789
/// editor sends new breakpoints (currently the editor just sends a new list
7890
/// and not requests to add/remove).
@@ -624,9 +636,24 @@ class IsolateManager {
624636
final existingBreakpoint = _clientBreakpointsByVmId[breakpointId];
625637
if (existingBreakpoint == null) {
626638
// If we can't match this breakpoint up, we cannot get its ID or send
627-
// events for it. This can happen if a breakpoint is being resolved just
628-
// as a user changes breakpoints, so we have replaced our collection with
629-
// a new set before we processed this event.
639+
// events for it. This can happen if:
640+
//
641+
// 1. A breakpoint is being resolved just as a user changes breakpoints,
642+
// so we have replaced our collection with a new set before we
643+
// processed this event.
644+
// 2. We got a `BreakpointAdded`/`BreakpointUpdated` event prior to the
645+
// response to `addBreakpointWithScriptUri`. In this case, we need to
646+
// process this once that has completed and we can match up the IDs.
647+
//
648+
// It's important we don't miss any events for (2), so queue them up so
649+
// they can be handled if/when the breakpoint is handled in a
650+
// `setBreakpointWithScriptUri` response.
651+
final queue = _pendingBreakpointEvents.putIfAbsent(
652+
breakpointId,
653+
PendingBreakpointActions.new,
654+
);
655+
// Queue another call back to this function later.
656+
queue.queueAction(() => _handleBreakpointAddedOrResolved(event));
630657
return;
631658
}
632659

@@ -791,8 +818,20 @@ class IsolateManager {
791818
final vmBp = await service.addBreakpointWithScriptUri(
792819
isolateId, vmUri.toString(), bp.breakpoint.line,
793820
column: bp.breakpoint.column);
794-
existingBreakpointsForIsolateAndUri[vmBp.id!] = vmBp;
795-
_clientBreakpointsByVmId[vmBp.id!] = bp;
821+
final vmBpId = vmBp.id!;
822+
existingBreakpointsForIsolateAndUri[vmBpId] = vmBp;
823+
_clientBreakpointsByVmId[vmBpId] = bp;
824+
825+
// Now we have the ID of this breakpoint, append an action that
826+
// processes any pending events.
827+
final pendingEvents = _pendingBreakpointEvents[vmBpId];
828+
if (pendingEvents != null) {
829+
bp.queueAction(() {
830+
pendingEvents.completer.complete();
831+
return pendingEvents._lastActionFuture;
832+
});
833+
_pendingBreakpointEvents.remove(vmBpId);
834+
}
796835
} catch (e) {
797836
// Swallow errors setting breakpoints rather than failing the whole
798837
// request as it's very easy for editors to send us breakpoints that
@@ -1254,6 +1293,30 @@ class ClientBreakpoint {
12541293
}
12551294
}
12561295

1296+
/// Tracks actions resulting from `BreakpointAdded`/`BreakpointResolved` events
1297+
/// that arrive before the `addBreakpointWithScriptUri` request completes.
1298+
///
1299+
/// These events need to be chained into the end of the [ClientBreakpoint] once
1300+
/// that request completes.
1301+
class PendingBreakpointActions {
1302+
/// A completer that will trigger processing of the queue.
1303+
final completer = Completer<void>();
1304+
1305+
/// A [Future] that completes with the last action in the queue.
1306+
late Future<void> _lastActionFuture;
1307+
1308+
PendingBreakpointActions() {
1309+
_lastActionFuture = completer.future;
1310+
}
1311+
1312+
/// Queues an action to run after all previous actions.
1313+
FutureOr<T> queueAction<T>(FutureOr<T> Function() action) {
1314+
final actionFuture = _lastActionFuture.then((_) => action());
1315+
_lastActionFuture = actionFuture;
1316+
return actionFuture;
1317+
}
1318+
}
1319+
12571320
class StoredData {
12581321
final ThreadInfo thread;
12591322
final Object data;

pkg/dds/test/dap/integration/debug_breakpoints_test.dart

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,45 @@ main() {
6464
expect(updatedBreakpoint.line, expectedResolvedBreakpointLine);
6565
});
6666

67+
test('resolves modified breakpoints', () async {
68+
final client = dap.client;
69+
final testFile = dap.createTestFile(simpleMultiBreakpointProgram);
70+
final breakpointLine = lineWith(testFile, breakpointMarker);
71+
72+
// Start the app and hit the initial breakpoint.
73+
await client.hitBreakpoint(testFile, breakpointLine);
74+
75+
// Collect IDs of all breakpoints that get resolved.
76+
final resolvedBreakpoints = <int>{};
77+
final breakpointResolveSubscription =
78+
client.breakpointChangeEvents.listen((event) {
79+
if (event.breakpoint.verified) {
80+
resolvedBreakpoints.add(event.breakpoint.id!);
81+
} else {
82+
resolvedBreakpoints.remove(event.breakpoint.id!);
83+
}
84+
});
85+
86+
// Add breakpoints to the 4 lines after the current one, one at a time.
87+
// Capture the IDs of all breakpoints added.
88+
final breakpointLinesToSend = <int>[breakpointLine];
89+
final addedBreakpoints = <int>{};
90+
for (var i = 1; i <= 4; i++) {
91+
breakpointLinesToSend.add(breakpointLine + i);
92+
final response =
93+
await client.setBreakpoints(testFile, breakpointLinesToSend);
94+
for (final breakpoint in response.breakpoints) {
95+
addedBreakpoints.add(breakpoint.id!);
96+
}
97+
}
98+
99+
await pumpEventQueue(times: 5000);
100+
await breakpointResolveSubscription.cancel();
101+
102+
// Ensure every breakpoint that was added was also resolved.
103+
expect(resolvedBreakpoints, addedBreakpoints);
104+
});
105+
67106
test('responds to setBreakpoints before any breakpoint events', () async {
68107
final client = dap.client;
69108
final testFile = dap.createTestFile(simpleBreakpointResolutionProgram);

pkg/dds/test/dap/integration/test_client.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -668,13 +668,18 @@ extension DapTestClientExtension on DapTestClient {
668668
}
669669

670670
/// Sets breakpoints at [lines] in [file].
671-
Future<void> setBreakpoints(File file, List<int> lines) async {
672-
await sendRequest(
671+
Future<SetBreakpointsResponseBody> setBreakpoints(
672+
File file, List<int> lines) async {
673+
final response = await sendRequest(
673674
SetBreakpointsArguments(
674675
source: Source(path: _normalizeBreakpointPath(file.path)),
675676
breakpoints: lines.map((line) => SourceBreakpoint(line: line)).toList(),
676677
),
677678
);
679+
680+
return SetBreakpointsResponseBody.fromJson(
681+
response.body as Map<String, Object?>,
682+
);
678683
}
679684

680685
/// Normalizes a non-breakpoint path being sent to the debug adapter based on

pkg/dds/test/dap/integration/test_scripts.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,20 @@ const simpleBreakpointProgram = '''
140140
}
141141
''';
142142

143+
/// A simple Dart script that prints the numbers from 1 to 5.
144+
///
145+
/// A breakpoint marker is on the line that prints '1' and the subsequent 4
146+
/// lines are valid targets for breakpoints.
147+
const simpleMultiBreakpointProgram = '''
148+
void main(List<String> args) async {
149+
print('1'); $breakpointMarker
150+
print('2');
151+
print('3');
152+
print('4');
153+
print('5');
154+
}
155+
''';
156+
143157
/// A Dart script that uses [Isolate.run] to run a short-lived isolate and has
144158
/// a `debugger()` call after the isolate completes to ensure the app does not
145159
/// immediately exit.

0 commit comments

Comments
 (0)