Last active
November 25, 2022 12:10
-
-
Save fzyzcjy/e68c375643d7c77942cdc8fb5f01de18 to your computer and use it in GitHub Desktop.
Test Flutter memory leaks at host
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)'); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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