Files
sdk/pkg/dds/test/cpu_sample_streaming_test.dart
T
Vyacheslav Egorov a6a99dc45f [vm] Fix issues in SampleBlockProcessor
This is follow up to commit cb0c2bf5ed.

SampleBlockProcessor only enters isolate group and not a specific
isolate so the code must not rely on thread->isolate(). This fixes two
places where this was not the case:

* ProfileBuilder::IsPCInDartHeap
* UserTags::TagName

pkg/dds/test/get_cached_cpu_samples_test was supposed to cover this but
it has two problems:

First I observed that SampleBlockProcessor never gets a chance to
process a block if mutator thread always gets to it first (via a
scheduled interrupt), so this code is not well exercised. I started by
adding a variant of the test where interrupts are inhibited via a
vm:unsafe:no-interrupts pragma - which revealed the crashes in the
SampleBlockProcessor code.

This revealed the second problem: get_cached_cpu_samples_test does not
actually fail if testee crashes during the test, it just silently
completes with success. This seems to happen because disposal of
VmService connection is not forwarded into the future on which the test
is awaiting - and the whole process just exits once VmService connection
to the testee disappears (because all ports are closed, no pending
activity is possible after that one). I have fixed this by adding a
helper function which checks that connection to VmService only goes away
when we dispose it.

Note: there is another obvious issue here, which I am leaving unfixed
for now. SampleBlockProcessor calls UserTags::TagName in a way that can
race with isolate itself modifying the table. I think this race is
extremely unlikely but it can cause crashes on ARMs with its weak memory
model (e.g. we might end up reading garbage due to the reordering of
stores).

TEST=pkg/dds/test/get_cached_cpu_samples_test

Change-Id: Iee15ec2b019928b798c312e63edc76696abf5527
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/426300
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Slava Egorov <vegorov@google.com>
2025-05-07 07:18:41 -07:00

147 lines
4.9 KiB
Dart

// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// VMOptions=
// VMOptions=-Ddisable.interrupts.to.test.sample.block.processor=true
import 'dart:async';
import 'dart:io';
import 'package:dds/dds.dart';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'common/test_helper.dart';
void main() {
late Process process;
DartDevelopmentService? dds;
setUp(() async {
process = await spawnDartProcess(
'get_cached_cpu_samples_script.dart',
disableServiceAuthCodes: true,
);
});
tearDown(() async {
await dds?.shutdown();
process.kill();
});
void declareTest(bool withDds) {
test(
'Stream CPU samples for provided UserTag names with${withDds ? "" : "out"} DDS',
() async {
Uri serviceUri = remoteVmServiceUri;
if (withDds) {
dds = await DartDevelopmentService.startDartDevelopmentService(
remoteVmServiceUri,
);
serviceUri = dds!.wsUri!;
expect(dds!.isRunning, true);
} else {
serviceUri = serviceUri.replace(scheme: 'ws', path: 'ws');
}
await withServiceConnection(serviceUri, (service) async {
await withServiceConnection(serviceUri, (otherService) async {
IsolateRef isolate;
while (true) {
final vm = await service.getVM();
if (vm.isolates!.isNotEmpty) {
isolate = vm.isolates!.first;
try {
isolate = await service.getIsolate(isolate.id!);
if ((isolate as Isolate).runnable!) {
break;
}
} on SentinelException {
// ignore
}
}
await Future.delayed(const Duration(seconds: 1));
}
expect(isolate, isNotNull);
final expectedUserTags = <String>{};
Future<void> listenForSamples() {
late StreamSubscription sub;
final completer = Completer<void>();
int i = 0;
sub = service.onProfilerEvent.listen(
(event) async {
if (event.kind == EventKind.kCpuSamples &&
event.isolate!.id! == isolate.id!) {
expect(expectedUserTags.isNotEmpty, true);
++i;
if (i > 3) {
if (!completer.isCompleted) {
await sub.cancel();
completer.complete();
}
return;
}
expect(event.cpuSamples, isNotNull);
final sampleCount = event.cpuSamples!.samples!
.where((e) => expectedUserTags.contains(e.userTag))
.length;
expect(sampleCount, event.cpuSamples!.samples!.length);
}
},
onDone: () {
if (!completer.isCompleted) {
completer.completeError(
StateError('unexpected end of the profiler stream'));
}
},
onError: (e) {
completer.completeError(e);
},
);
if (expectedUserTags.isEmpty) {
return Future.delayed(const Duration(seconds: 2)).then(
(_) async => await sub.cancel(),
);
}
return completer.future;
}
await service.streamListen(EventStreams.kProfiler);
Future<void> subscription = listenForSamples();
await service.resume(isolate.id!);
await subscription;
await service.pause(isolate.id!);
expectedUserTags.add('Testing');
await service
.streamCpuSamplesWithUserTag(expectedUserTags.toList());
subscription = listenForSamples();
await service.resume(isolate.id!);
await subscription;
await service.pause(isolate.id!);
expectedUserTags.add('Baz');
await service
.streamCpuSamplesWithUserTag(expectedUserTags.toList());
subscription = listenForSamples();
await service.resume(isolate.id!);
await subscription;
await service.pause(isolate.id!);
expectedUserTags.clear();
await service
.streamCpuSamplesWithUserTag(expectedUserTags.toList());
subscription = listenForSamples();
await service.resume(isolate.id!);
await subscription;
});
});
},
timeout: Timeout.none,
);
}
declareTest(true);
declareTest(false);
}