I am trying to simulate a Memory Leak for Streams (for educational purposes), but I can't be sure memory is actually leaking if the GC hasn't run yet.
Can I force a GC sweep in a pure Dart test?
I am trying to simulate a Memory Leak for Streams (for educational purposes), but I can't be sure memory is actually leaking if the GC hasn't run yet.
Can I force a GC sweep in a pure Dart test?
You can do it from the Dart DevTools Web GUI. https://dart.dev/tools/dart-devtools
With newer Dart versions, we can use the dart:developer API to communicate with the Dart VM. This is also how the new Dart DevTools are getting data from the VM and make operations like triggering the garbage collector which you can still do manually by going into the web interface.
If you want to trigger this from the code itself, you can do that using the vm_service package which makes it easy to interact with the API from the Dart VM Service Protocol.
An example can be seen here:
import 'dart:developer';
import 'dart:isolate';
import 'package:vm_service/vm_service_io.dart';
Future<void> main(List<String> args) async {
final serverUri = (await Service.getInfo()).serverUri;
if (serverUri == null) {
print('Please run the application with the --observe parameter!');
return;
}
final isolateId = Service.getIsolateID(Isolate.current)!;
final vmService = await vmServiceConnectUri(_toWebSocket(serverUri));
final profile = await vmService.getAllocationProfile(isolateId, gc: true);
print(profile.memoryUsage?.heapUsage);
}
List<String> _cleanupPathSegments(Uri uri) {
final pathSegments = <String>[];
if (uri.pathSegments.isNotEmpty) {
pathSegments.addAll(uri.pathSegments.where(
(s) => s.isNotEmpty,
));
}
return pathSegments;
}
String _toWebSocket(Uri uri) {
final pathSegments = _cleanupPathSegments(uri);
pathSegments.add('ws');
return uri.replace(scheme: 'ws', pathSegments: pathSegments).toString();
}
The getAllocationProfile method takes an optional argument gc which is documented as:
If
gcis provided and is set to true, a garbage collection will be attempted before collecting allocation information. There is no guarantee that a garbage collection will be actually be performed.
So this is really the best you can do. Also, this solution will only work if the program has been started with the --observe parameter. So this is not something you should use for other than debugging purposes.
For those who wants to trigger GC in Dart/Flutter tests (e.g. dart test my_file.dart), here is my solution based on @julemand101:
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'dart:isolate';
import 'package:common_dart/utils/processes.dart';
import 'package:front_log/front_log.dart';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart' hide Isolate, Log;
import 'package:vm_service/vm_service.dart' as vm_service;
import 'package:vm_service/vm_service_io.dart';
const _kTag = 'vm_services';
FutureOr<void> runTestsInVmService(
FutureOr<void> Function(VmServiceUtil) body, {
required String selfFilePath,
}) async {
Log.d(_kTag, 'runInVmService selfFilePath=$selfFilePath Platform.script.path=${Platform.script.path}');
if (Platform.script.path == selfFilePath) {
final vmService = await VmServiceUtil.create();
tearDownAll(vmService.dispose);
await body(vmService);
} else {
test('run all tests in subprocess', () async {
await executeProcess('dart', ['run', '--enable-vm-service', selfFilePath]);
});
}
}
class VmServiceUtil {
static const _kTag = 'VmServiceUtil';
final VmService vmService;
VmServiceUtil._(this.vmService);
static Future<VmServiceUtil> create() async {
final serverUri = (await Service.getInfo()).serverUri;
if (serverUri == null) {
throw Exception('Cannot find serverUri for VmService. '
'Ensure you run like `dart run --enable-vm-service path/to/your/file.dart`');
}
final vmService = await vmServiceConnectUri(_toWebSocket(serverUri), log: _Log());
return VmServiceUtil._(vmService);
}
void dispose() {
vmService.dispose();
}
Future<void> gc() async {
final isolateId = Service.getIsolateID(Isolate.current)!;
final profile = await vmService.getAllocationProfile(isolateId, gc: true);
Log.d(_kTag, 'gc triggered (heapUsage=${profile.memoryUsage?.heapUsage})');
}
}
String _toWebSocket(Uri uri) {
final pathSegments = [...uri.pathSegments.where((s) => s.isNotEmpty), 'ws'];
return uri.replace(scheme: 'ws', pathSegments: pathSegments).toString();
}
class _Log extends vm_service.Log {
static const _kTag = 'vm_services';
@override
void warning(String message) => Log.w(_kTag, message);
@override
void severe(String message) => Log.e(_kTag, message);
}
Future<void> executeProcess(String executable, List<String> arguments) async {
Log.d(_kTag, 'executeProcess start `$executable ${arguments.join(" ")}`');
final process = await Process.start(executable, arguments);
process.stdout.listen((e) => Log.d(_kTag, String.fromCharCodes(e)));
process.stderr.listen((e) => Log.d(_kTag, '[STDERR] ${String.fromCharCodes(e)}'));
// stdout.addStream(process.stdout);
// stderr.addStream(process.stderr);
final exitCode = await process.exitCode;
Log.d(_kTag, 'executeProcess end exitCode=$exitCode');
if (exitCode != 0) {
throw Exception('Process execution failed (exitCode=$exitCode)');
}
}
How to use it:
void main() {
runTestsInVmService(
_core,
selfFilePath: 'path/to/my/file.dart',
);
}
void _core(VmServiceUtil vmService) {
test('hello', () async {
do_something();
await vmService.gc();
do_another_thing();
});
}