Skip to content

Instantly share code, notes, and snippets.

@fzyzcjy
Last active November 25, 2022 12:10
Show Gist options
  • Save fzyzcjy/e68c375643d7c77942cdc8fb5f01de18 to your computer and use it in GitHub Desktop.
Save fzyzcjy/e68c375643d7c77942cdc8fb5f01de18 to your computer and use it in GitHub Desktop.
Test Flutter memory leaks at host
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';
// #4657
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',
// #4764
timeout: const Timeout(Duration(seconds: 60)),
() async {
await executeProcess('dart', ['run', '--enable-vm-service', selfFilePath]);
},
);
}
}
/// https://stackoverflow.com/questions/63730179/can-we-force-the-dart-garbage-collector
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 {
@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)');
}
}
// ignore_for_file: avoid_print
import 'dart:typed_data';
import 'package:common_dart/utils/functionals.dart';
import 'package:common_dart/utils/host_info.dart';
import 'package:common_dart/utils/mobx/reactive_future_resource_v2.dart';
import 'package:common_dart_dev/common_dart_dev.dart';
import 'package:mobx/mobx.dart';
import 'package:test/test.dart';
void main() {
runTestsInVmService(
_core,
selfFilePath: 'path/to/this/file.dart',
);
}
void _core(VmServiceUtil vmService) {
group('ReactiveFutureResource vm service test', () {
test('simple gc and WeakReference case', () async {
Uint8List? strongRef = _createLargeList(mb: 50);
final weakRef = WeakReference(strongRef);
expect(strongRef, isNotNull);
expect(weakRef.target, isNotNull);
strongRef = null;
await vmService.gc();
expect(strongRef, isNull);
expect(weakRef.target, isNull);
});
group('when dispose, should clear to avoid memory-leak', () {
for (final readWithinReactiveEnvironment in [false, true]) {
group('readWithinReactiveEnvironment=$readWithinReactiveEnvironment', () {
const delayBeforeDispose = Duration(seconds: 1);
Future<void> _body({
required Future<void> nonReactiveTaskAwaitFuture,
}) async {
Uint8List? strongRef = _createLargeList(mb: 300);
final weakRef = WeakReference(strongRef);
_log('body construct asyncComputed');
final asyncComputed = ReactiveFutureResource<void, Uint8List?>(
() {},
(_) async {
_log('NonReactiveTask start (waiting for future)');
await nonReactiveTaskAwaitFuture;
_log('NonReactiveTask end (return big list)');
final ans = strongRef!;
strongRef = null;
return ans;
},
);
if (readWithinReactiveEnvironment) {
final autorunDisposer = autorun((_) {
final _ = asyncComputed.maybeStaleValue;
_log('autorun is called');
});
addTearDown(autorunDisposer);
} else {
final _ = asyncComputed.maybeStaleValue;
_log('trigger read to maybeStaleValue once');
}
await Future<void>.delayed(delayBeforeDispose);
_log('body call dispose');
asyncComputed.dispose();
_log('body await nonReactiveTaskAwaitFuture such that task is finished');
await nonReactiveTaskAwaitFuture;
await Future<void>.delayed(const Duration(milliseconds: 100));
expect(strongRef, isNull);
await _retry(() async {
await vmService.gc();
await Future<void>.delayed(const Duration(milliseconds: 10));
// debugger();
return weakRef.target == null;
}, 'the big list should not be referenced anymore and is GCed');
}
test('when async task has already finished running at the time of dispose', () async {
await _body(
nonReactiveTaskAwaitFuture: Future.value(),
);
});
test('when async task is still running at the time of dispose', () async {
await _body(
nonReactiveTaskAwaitFuture: Future.delayed(delayBeforeDispose + const Duration(seconds: 1)),
);
});
});
}
});
});
}
Future<void> _retry(Future<bool> Function() body, String reason, [int maxAttempts = 10]) async {
for (final _ in range(maxAttempts)) {
if (await body()) return;
}
throw AssertionError('retry failed after $maxAttempts, reason=$reason');
}
Uint8List _createLargeList({required int mb}) => Uint8List(1000000 * mb);
void _log(String msg) => print('[${DateTime.now()}]\t$msg');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment