Skip to content

Commit 5b9d75f

Browse files
DanTupCommit Queue
authored andcommitted
[dds/dap] Use ID Zones for evaluation requests and invalidate them on resume
Fixes #56794 Change-Id: Ied408503d22c4abb2c4f996f6c9b7847b8ad82be Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/389960 Reviewed-by: Derek Xu <[email protected]> Commit-Queue: Ben Konyi <[email protected]> Reviewed-by: Ben Konyi <[email protected]>
1 parent 901f356 commit 5b9d75f

File tree

10 files changed

+362
-40
lines changed

10 files changed

+362
-40
lines changed

pkg/dds/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
- Updated the `devtools_shared` dependency to version `^11.0.0`.
44
- Made `runDartDevelopmentServiceFromCLI` pass the specified bind address
55
directly into `startDartDevelopmentService` without resolving the address.
6+
- [DAP] Evaluations now use Service ID Zones to more precisely control the
7+
lifetime of instance references returned. This should avoid instances being
8+
collected while execution is paused, while releasing them once execution
9+
resumes.
10+
- Updated `vm_service` constraint to ^14.3.0.
611

712
# 4.2.7
813
- Added a new constant `RpcErrorCodes.kConnectionDisposed = -32010` for requests

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

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,22 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
956956
sendResponse(_noResult);
957957
break;
958958

959+
// Used by tests to force a GC for a given DAP threadId.
960+
case '_collectAllGarbage':
961+
final threadId = args?.args['threadId'] as int;
962+
final isolateId = isolateManager.getThread(threadId)?.isolate.id;
963+
// Trigger the GC.
964+
if (isolateId != null) {
965+
await vmService?.callMethod(
966+
'_collectAllGarbage',
967+
isolateId: isolateId,
968+
);
969+
}
970+
971+
// Respond to the incoming request.
972+
sendResponse(_noResult);
973+
break;
974+
959975
default:
960976
await super.customRequest(request, args, sendResponse);
961977
}
@@ -1088,11 +1104,10 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
10881104
thread,
10891105
);
10901106
} else if (thread != null && frameIndex != null) {
1091-
result = await vmService?.evaluateInFrame(
1092-
thread.isolate.id!,
1107+
result = await vmEvaluateInFrame(
1108+
thread,
10931109
frameIndex,
10941110
expression,
1095-
disableBreakpoints: true,
10961111
);
10971112
} else if (targetScriptFileUri != null &&
10981113
// Since we can't currently get a thread, we assume the first thread is
@@ -1105,12 +1120,7 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
11051120
throw 'Unable to find the library for $targetScriptFileUri';
11061121
}
11071122

1108-
result = await vmService?.evaluate(
1109-
thread.isolate.id!,
1110-
library.id!,
1111-
expression,
1112-
disableBreakpoints: true,
1113-
);
1123+
result = await vmEvaluate(thread, library.id!, expression);
11141124
}
11151125
} catch (e) {
11161126
final rawMessage = '$e';
@@ -2384,11 +2394,54 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
23842394
final expressionWithoutExceptionExpression =
23852395
expression.substring(threadExceptionExpression.length + 1);
23862396

2387-
return vmService?.evaluate(
2388-
thread.isolate.id!,
2397+
return vmEvaluate(
2398+
thread,
23892399
exception.id!,
23902400
expressionWithoutExceptionExpression,
2391-
disableBreakpoints: true,
2401+
);
2402+
}
2403+
2404+
/// Sends a VM 'evaluate' request for [thread].
2405+
Future<vm.Response?> vmEvaluate(
2406+
ThreadInfo thread,
2407+
String targetId,
2408+
String expression, {
2409+
bool? disableBreakpoints = true,
2410+
}) async {
2411+
final isolateId = thread.isolate.id!;
2412+
final futureOrEvalZoneId = thread.currentEvaluationZoneId;
2413+
final evalZoneId = futureOrEvalZoneId is String
2414+
? futureOrEvalZoneId
2415+
: await futureOrEvalZoneId;
2416+
2417+
return vmService?.evaluate(
2418+
isolateId,
2419+
targetId,
2420+
expression,
2421+
disableBreakpoints: disableBreakpoints,
2422+
idZoneId: evalZoneId,
2423+
);
2424+
}
2425+
2426+
/// Sends a VM 'evaluateInFrame' request for [thread].
2427+
Future<vm.Response?> vmEvaluateInFrame(
2428+
ThreadInfo thread,
2429+
int frameIndex,
2430+
String expression, {
2431+
bool? disableBreakpoints = true,
2432+
}) async {
2433+
final isolateId = thread.isolate.id!;
2434+
final futureOrEvalZoneId = thread.currentEvaluationZoneId;
2435+
final evalZoneId = futureOrEvalZoneId is String
2436+
? futureOrEvalZoneId
2437+
: await futureOrEvalZoneId;
2438+
2439+
return vmService?.evaluateInFrame(
2440+
isolateId,
2441+
frameIndex,
2442+
expression,
2443+
disableBreakpoints: disableBreakpoints,
2444+
idZoneId: evalZoneId,
23922445
);
23932446
}
23942447

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

Lines changed: 95 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ class IsolateManager {
356356

357357
// Finally, when we're resuming, all stored objects become invalid and
358358
// we can drop them to save memory.
359-
thread.clearStoredData();
359+
await thread.clearTemporaryData();
360360

361361
thread.hasPendingUserResume = true;
362362
try {
@@ -400,13 +400,14 @@ class IsolateManager {
400400
}
401401
}
402402

403-
// When we're resuming, all stored objects become invalid and we can drop
404-
// to save memory.
405-
thread.clearStoredData();
406-
403+
final isolateId = thread.isolate.id!;
407404
try {
408-
thread.hasPendingDapResume = true;
409-
await _adapter.vmService?.readyToResume(thread.isolate.id!);
405+
// When we're resuming, all stored objects become invalid and we can drop
406+
// to save memory.
407+
await thread.clearTemporaryData();
408+
409+
// Finally, signal that we're ready to resume.
410+
await _adapter.vmService?.readyToResume(isolateId);
410411
} on UnimplementedError {
411412
// Fallback to a regular resume if the DDS version doesn't support
412413
// `readyToResume`:
@@ -426,8 +427,6 @@ class IsolateManager {
426427
} else {
427428
rethrow;
428429
}
429-
} finally {
430-
thread.hasPendingDapResume = false;
431430
}
432431
}
433432

@@ -570,12 +569,7 @@ class IsolateManager {
570569
String type,
571570
) async {
572571
try {
573-
final result = await _adapter.vmService?.evaluateInFrame(
574-
thread.isolate.id!,
575-
0,
576-
expression,
577-
disableBreakpoints: true,
578-
);
572+
final result = await _adapter.vmEvaluateInFrame(thread, 0, expression);
579573

580574
if (result is vm.InstanceRef) {
581575
return result;
@@ -1118,6 +1112,32 @@ class ThreadInfo with FileUtils {
11181112
var atAsyncSuspension = false;
11191113
int? exceptionReference;
11201114

1115+
/// A [Completer] that completes with the evaluation zone ID for this thread.
1116+
///
1117+
/// The completer is created when the request to create an evaluation zone is
1118+
/// started (which is lazy, the first time evaluation is performed).
1119+
///
1120+
/// When the Debug Adapter is ready to resume this Isolate, it will first
1121+
/// invalidate all evaluation IDs in this zone so that they can be collected.
1122+
/// If the [Completer] is null, no evaluation has occurred and invalidation
1123+
/// can be skipped.
1124+
Completer<String?>? _currentEvaluationZoneIdCompleter;
1125+
1126+
/// Returns the current evaluation zone ID.
1127+
///
1128+
/// To avoid additional 'await's, may return a String? directly if the value
1129+
/// is already available.
1130+
FutureOr<String?> get currentEvaluationZoneId {
1131+
// We already have the value, avoid the Future.
1132+
if (_currentEvaluationZoneId != null) {
1133+
return _currentEvaluationZoneId;
1134+
}
1135+
return _createOrGetEvaluationZoneId();
1136+
}
1137+
1138+
/// The current evaluation zone ID (if available).
1139+
String? _currentEvaluationZoneId;
1140+
11211141
/// Whether this thread is currently known to be paused in the VM.
11221142
///
11231143
/// Because requests are async, this is not guaranteed to be always correct
@@ -1165,10 +1185,6 @@ class ThreadInfo with FileUtils {
11651185
/// has not yet been responded to.
11661186
var hasPendingUserResume = false;
11671187

1168-
/// Whether this isolate has an in-flight DAP (readyToResume) resume request
1169-
/// that has not yet been responded to.
1170-
var hasPendingDapResume = false;
1171-
11721188
ThreadInfo(this._manager, this.threadId, this.isolate);
11731189

11741190
Future<T> getObject<T extends vm.Response>(vm.ObjRef ref) =>
@@ -1188,6 +1204,42 @@ class ThreadInfo with FileUtils {
11881204
return _manager.getScripts(isolate);
11891205
}
11901206

1207+
/// Returns the evaluation zone ID for this thread.
1208+
///
1209+
/// If it has not been created yet, creates it. If creation is in progress,
1210+
/// returns the existing future.
1211+
Future<String?> _createOrGetEvaluationZoneId() async {
1212+
// If we already have a completer, the request is already in flight (or
1213+
// has completed).
1214+
var completer = _currentEvaluationZoneIdCompleter;
1215+
if (completer != null) {
1216+
return completer.future;
1217+
}
1218+
1219+
// Otherwise, we need to start the request.
1220+
_currentEvaluationZoneIdCompleter = completer = Completer();
1221+
1222+
try {
1223+
final response = await _manager._adapter.vmService?.createIdZone(
1224+
isolate.id!,
1225+
vm.IdZoneBackingBufferKind.kRing,
1226+
vm.IdAssignmentPolicy.kAlwaysAllocate,
1227+
// Default capacity is 512. Since these are short-lived (only while
1228+
// paused) and we don't want to prevent expanding Lists, use something a
1229+
// little bigger.
1230+
capacity: 2048,
1231+
);
1232+
_currentEvaluationZoneId = response?.id;
1233+
} catch (_) {
1234+
// If this request fails for any reason (perhaps the target VM does not
1235+
// support this request), we should just use `null` as the zone ID and not
1236+
// prevent any evaluation requests.
1237+
_currentEvaluationZoneId = null;
1238+
}
1239+
completer.complete(_currentEvaluationZoneId);
1240+
return _currentEvaluationZoneId;
1241+
}
1242+
11911243
/// Resolves a source file path (or URI) into a URI for the VM.
11921244
///
11931245
/// sdk-path/lib/core/print.dart -> dart:core/print.dart
@@ -1489,11 +1541,32 @@ class ThreadInfo with FileUtils {
14891541
pathSegments: fileLikeUri.pathSegments.sublist(0, keepSegments));
14901542
}
14911543

1492-
/// Clears all data stored for this thread.
1544+
/// Clears all temporary stored for this thread. This includes:
1545+
///
1546+
/// - dropping any variablesReferences
1547+
/// - invalidating the evaluation ID zone
1548+
///
1549+
/// This is generally called when requesting execution continues, since any
1550+
/// evaluated references are not expected to live past this point.
14931551
///
1494-
/// References to stored data become invalid when the thread is resumed.
1495-
void clearStoredData() {
1552+
/// https://microsoft.github.io/debug-adapter-protocol/overview#lifetime-of-objects-references
1553+
Future<void> clearTemporaryData() async {
1554+
// Clear variablesReferences.
14961555
_manager.clearStoredData(this);
1556+
1557+
// Invalidate all existing references in this evaluation zone.
1558+
// If the completer is null, no zone has ever been created (or started to
1559+
// be created), so this can be skipped.
1560+
if (_currentEvaluationZoneIdCompleter != null) {
1561+
final futureOrEvalZoneId = currentEvaluationZoneId;
1562+
final evalZoneId = futureOrEvalZoneId is String
1563+
? futureOrEvalZoneId
1564+
: await futureOrEvalZoneId;
1565+
if (evalZoneId != null) {
1566+
await _manager._adapter.vmService
1567+
?.invalidateIdZone(isolate.id!, evalZoneId);
1568+
}
1569+
}
14971570
}
14981571

14991572
/// Attempts to get a [vm.LibraryRef] for the given [scriptFileUri].

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,8 @@ class ProtocolConverter {
335335
required bool allowCallingToString,
336336
required VariableFormat? format,
337337
}) async {
338-
final response = await service.evaluate(
339-
thread.isolate.id!,
338+
final response = await _adapter.vmEvaluate(
339+
thread,
340340
instance.id!,
341341
getterName,
342342
);

pkg/dds/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ dependencies:
2929
sse: ^4.0.0
3030
stack_trace: ^1.10.0
3131
stream_channel: ^2.0.0
32-
vm_service: ^14.0.0
32+
vm_service: ^14.3.0
3333
web_socket_channel: '>=2.0.0 <4.0.0'
3434

3535
# We use 'any' version constraints here as we get our package versions from

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,7 @@ void main(List<String> args) async {
606606
final thread1 = stop1.threadId!;
607607

608608
// Attach a second debug adapter to it.
609-
final dap2 = await DapTestSession.setUp();
609+
final dap2 = await DapTestSession.setUp(logPrefix: '(CLIENT2) ');
610610
final client2 = dap2.client;
611611
await Future.wait([
612612
// We'll still get event for existing pause.

0 commit comments

Comments
 (0)