Skip to content

Commit df6fd34

Browse files
mralephCommit Queue
authored and
Commit Queue
committed
[vm] Improve asynchronous unwinding through Stream methods
This CL adds @pragma('vm:awaiter-link') in various places in Stream implementation to facilitate unwinding and expands async unwinding logic with more information about Stream internals. At the same time be more conservative when checking if an exception thrown from async method handled: failing to unwind the stack fully creates situations when we incorrectly report caught exceptions as uncaught, which frustrates users. To distinguish stream subscriptions with and without error handlers we add a state bit. Otherwise, it looks like all subscriptions have error handlers because if no error handler is installed we eagerly install error handler forwarding the error to `Zone.handleUncaughtError`. Fixes #53334 Fixes #54788 Fixes #47985 TEST=runtime/vm/dart/awaiter_stacks/stream_methods_test.dart,pkg/vm_service/test/pause_on_unhandled_async_exceptions6_test.dart,pkg/vm_service/test/pause_on_unhandled_async_exceptions7_test.dart CoreLibraryReviewExempt: No behavioral change. Async changes reviewed by lrhn@ Cq-Include-Trybots: luci.dart.try:vm-aot-android-release-arm64c-try,vm-aot-android-release-arm_x64-try,vm-aot-dwarf-linux-product-x64-try,vm-aot-obfuscate-linux-release-x64-try,vm-aot-optimization-level-linux-release-x64-try Change-Id: Ic51f926867092dd0adbe801b753f57c357c7ace2 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/322720 Reviewed-by: Alexander Markov <[email protected]> Reviewed-by: Lasse Nielsen <[email protected]> Reviewed-by: Ben Konyi <[email protected]> Commit-Queue: Slava Egorov <[email protected]>
1 parent 25407d3 commit df6fd34

14 files changed

+2248
-148
lines changed

pkg/vm_service/test/common/service_test_common.dart

+149-33
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,62 @@ IsolateTest setBreakpointAtLineColumn(int line, int column) {
211211
};
212212
}
213213

214+
extension FrameLocation on Frame {
215+
Future<(String, int)> getLocation(
216+
VmService service,
217+
IsolateRef isolateRef,
218+
) async {
219+
if (location?.tokenPos == null) {
220+
return ('<unknown>', -1);
221+
}
222+
223+
final script = (await service.getObject(
224+
isolateRef.id!,
225+
location!.script!.id!,
226+
)) as Script;
227+
return (
228+
script.uri!,
229+
script.getLineNumberFromTokenPos(location!.tokenPos!) ?? -1
230+
);
231+
}
232+
}
233+
234+
Future<String> formatFrames(
235+
VmService service,
236+
IsolateRef isolateRef,
237+
List<Frame> frames,
238+
) async {
239+
final sb = StringBuffer();
240+
for (Frame f in frames) {
241+
sb.write(' $f');
242+
if (f.function case final funcRef?) {
243+
sb.write(' ');
244+
sb.write(await qualifiedFunctionName(service, isolateRef, funcRef));
245+
}
246+
if (f.location != null) {
247+
final (uri, lineNo) = await f.getLocation(service, isolateRef);
248+
sb.write(' $uri:$lineNo');
249+
}
250+
sb.writeln();
251+
}
252+
return sb.toString();
253+
}
254+
255+
Future<String> formatStack(
256+
VmService service,
257+
IsolateRef isolateRef,
258+
Stack stack,
259+
) async {
260+
final sb = StringBuffer();
261+
sb.write('Full stack trace:\n');
262+
sb.writeln(await formatFrames(service, isolateRef, stack.frames!));
263+
if (stack.asyncCausalFrames case final asyncFrames?) {
264+
sb.write('\nFull async stack trace:\n');
265+
sb.writeln(await formatFrames(service, isolateRef, asyncFrames));
266+
}
267+
return sb.toString();
268+
}
269+
214270
IsolateTest stoppedAtLine(int line) {
215271
return (VmService service, IsolateRef isolateRef) async {
216272
print('Checking we are at line $line');
@@ -227,33 +283,12 @@ IsolateTest stoppedAtLine(int line) {
227283
expect(frames.length, greaterThanOrEqualTo(1));
228284

229285
final top = frames[0];
230-
final Script script =
231-
(await service.getObject(id, top.location!.script!.id!)) as Script;
232-
final int actualLine =
233-
script.getLineNumberFromTokenPos(top.location!.tokenPos!)!;
286+
final (_, actualLine) = await top.getLocation(service, isolateRef);
234287
if (actualLine != line) {
235288
print('Actual: $actualLine Line: $line');
236289
final sb = StringBuffer();
237290
sb.write('Expected to be at line $line but actually at line $actualLine');
238-
sb.write('\nFull stack trace:\n');
239-
for (Frame f in frames) {
240-
sb.write(
241-
' $f [${script.getLineNumberFromTokenPos(f.location!.tokenPos!)}]\n',
242-
);
243-
}
244-
if (stack.asyncCausalFrames != null) {
245-
final asyncFrames = stack.asyncCausalFrames!;
246-
sb.write('\nFull async stack trace:\n');
247-
for (Frame f in asyncFrames) {
248-
sb.write(' $f');
249-
if (f.location != null) {
250-
sb.write(
251-
' [${script.getLineNumberFromTokenPos(f.location!.tokenPos!)}]',
252-
);
253-
}
254-
sb.writeln();
255-
}
256-
}
291+
sb.writeln(await formatStack(service, isolateRef, stack));
257292
throw sb.toString();
258293
} else {
259294
print('Program is stopped at line: $line');
@@ -708,22 +743,35 @@ IsolateTest stoppedInFunction(String functionName) {
708743
'Expected to be in function $functionName but '
709744
'actually in function $name',
710745
);
711-
sb.writeln('Full stack trace:');
712-
for (final frame in frames) {
713-
final func = await service.getObject(
714-
isolateId,
715-
frame.function!.id!,
716-
) as Func;
717-
final ownerName = func.owner.name!;
718-
sb.write(' $frame [${func.name}] [$ownerName]\n');
719-
}
746+
sb.writeln(await formatStack(service, isolateRef, stack));
747+
720748
throw sb.toString();
721749
} else {
722750
print('Program is stopped in function: $functionName');
723751
}
724752
};
725753
}
726754

755+
Future<String> qualifiedFunctionName(
756+
VmService service,
757+
IsolateRef isolate,
758+
FuncRef func,
759+
) async {
760+
final funcName = func.name ?? '<unknown>';
761+
switch (func.owner) {
762+
case final FuncRef parentFuncRef:
763+
final parentFuncName =
764+
await qualifiedFunctionName(service, isolate, parentFuncRef);
765+
return '$parentFuncName.$funcName';
766+
767+
case final ClassRef parentClass:
768+
return '${parentClass.name!}.$funcName';
769+
770+
case _:
771+
return funcName;
772+
}
773+
}
774+
727775
Future<void> expectFrame(
728776
VmService service,
729777
IsolateRef isolate,
@@ -734,7 +782,10 @@ Future<void> expectFrame(
734782
}) async {
735783
expect(frame.kind, equals(kind));
736784
if (functionName != null) {
737-
expect(frame.function?.name, equals(functionName));
785+
expect(
786+
await qualifiedFunctionName(service, isolate, frame.function!),
787+
equals(functionName),
788+
);
738789
}
739790
if (line != null) {
740791
expect(frame.location, isNotNull);
@@ -749,3 +800,68 @@ Future<void> expectFrame(
749800
);
750801
}
751802
}
803+
804+
Future<String> getCurrentExceptionAsString(
805+
VmService service,
806+
IsolateRef isolateRef,
807+
) async {
808+
final isolate = await service.getIsolate(isolateRef.id!);
809+
final event = isolate.pauseEvent!;
810+
final exception = await service.getObject(
811+
isolateRef.id!,
812+
event.exception!.id!,
813+
) as Instance;
814+
return exception.valueAsString!;
815+
}
816+
817+
typedef ExpectedFrame = ({String? functionName, int? line});
818+
const ExpectedFrame asyncGap = (functionName: null, line: null);
819+
820+
IsolateTest resumePastUnhandledException(String exceptionAsString) {
821+
return (service, isolateRef) async {
822+
do {
823+
await resumeIsolate(service, isolateRef);
824+
await hasStoppedWithUnhandledException(service, isolateRef);
825+
} while (await getCurrentExceptionAsString(service, isolateRef) ==
826+
exceptionAsString);
827+
};
828+
}
829+
830+
IsolateTest expectUnhandledExceptionWithFrames({
831+
List<ExpectedFrame>? expectedFrames,
832+
String? exceptionAsString,
833+
}) {
834+
return (VmService service, IsolateRef isolateRef) async {
835+
await hasStoppedWithUnhandledException(service, isolateRef);
836+
if (exceptionAsString != null) {
837+
expect(
838+
await getCurrentExceptionAsString(service, isolateRef),
839+
equals(exceptionAsString),
840+
);
841+
}
842+
843+
if (expectedFrames == null) {
844+
return;
845+
}
846+
847+
final stack = await service.getStack(isolateRef.id!);
848+
849+
final frames = stack.asyncCausalFrames!;
850+
var currentKind = 'Regular';
851+
for (var i = 0; i < expectedFrames.length; i++) {
852+
final expected = expectedFrames[i];
853+
final got = frames[i];
854+
await expectFrame(
855+
service,
856+
isolateRef,
857+
got,
858+
kind: expected == asyncGap ? 'AsyncSuspensionMarker' : currentKind,
859+
functionName: expected.functionName,
860+
line: expected.line,
861+
);
862+
if (expected == asyncGap) {
863+
currentKind = 'AsyncCausal';
864+
}
865+
}
866+
};
867+
}

pkg/vm_service/test/common/test_helper.dart

+10-1
Original file line numberDiff line numberDiff line change
@@ -337,8 +337,17 @@ class _ServiceTesterRunner {
337337
timeout: Timeout.none,
338338
);
339339

340-
tearDown(() {
340+
tearDown(() async {
341341
print('All service tests completed successfully.');
342+
try {
343+
await vm.dispose();
344+
} catch (e, st) {
345+
print('''
346+
Ignoring exception during vm-service connection shutdown:
347+
$e
348+
$st
349+
''');
350+
}
342351
process.requestExit();
343352
});
344353

0 commit comments

Comments
 (0)