blob: cdecc48e8e06f6f691e53682356b479b61984bbd [file] [log] [blame]
// Copyright (c) 2022, 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.
import 'dart:async';
import 'dart:developer';
import 'package:clock/clock.dart';
import '../shared/shared_model.dart';
import '_gc_counter.dart';
import 'leak_tracker.dart';
import 'leak_tracker_model.dart';
/// Asynchronous callback.
///
/// The prefix `Dart` is used to avoid conflict with Flutter's [AsyncCallback].
typedef DartAsyncCallback = Future<void> Function();
typedef AsyncCodeRunner = Future<void> Function(DartAsyncCallback);
class MemoryLeaksDetectedError extends StateError {
MemoryLeaksDetectedError(this.leaks) : super('Leaks detected.');
final Leaks leaks;
}
/// Runs [callback] with memory leak detection.
///
/// See https://github.com/dart-lang/leak_tracker
/// for memory leak definition.
///
/// If leaks are not detected, returns empty collection of leaks.
///
/// If leaks are detected, either returns collection of leaks
/// or throws [MemoryLeaksDetectedError], depending on value of [shouldThrowOnLeaks].
///
/// Flip [shouldThrowOnLeaks] to false if you want to test cover the leak tracker or
/// analyze leak details in the returned result.
///
/// Pass [timeoutForFinalGarbageCollection] if you do not want leak tracker
/// to wait infinitely for the forced garbage collection, that is needed
/// to analyse results.
///
/// If you test Flutter widgets, connect their instrumentation to the leak
/// tracker:
/// ```
/// void flutterEventListener(ObjectEvent event) => dispatchObjectEvent(event.toMap());
///
/// setUpAll(() {
/// MemoryAllocations.instance.addListener(flutterEventListener);
/// });
///
/// tearDownAll(() {
/// MemoryAllocations.instance.removeListener(flutterEventListener);
/// });
/// ```
///
/// If you use [withLeakTracking] inside [testWidget], pass [tester.runAsync]
/// as [asyncCodeRunner] to run asynchronous leak detection after the
/// test code execution:
///
/// ```
/// testWidgets('...', (WidgetTester tester) async {
/// await withLeakTracking(
/// () async {
/// ...
/// },
/// asyncCodeRunner: (action) async => tester.runAsync(action),
/// );
/// });
/// ```
Future<Leaks> withLeakTracking(
DartAsyncCallback? callback, {
bool shouldThrowOnLeaks = true,
Duration? timeoutForFinalGarbageCollection,
LeakDiagnosticConfig leakDiagnosticConfig = const LeakDiagnosticConfig(),
AsyncCodeRunner? asyncCodeRunner,
}) async {
if (callback == null) return Leaks({});
enableLeakTracking(
resetIfAlreadyEnabled: true,
config: LeakTrackingConfiguration.passive(
leakDiagnosticConfig: leakDiagnosticConfig,
),
);
try {
await callback();
callback = null;
asyncCodeRunner ??= (action) => action();
late Leaks leaks;
await asyncCodeRunner(
() async {
if (leakDiagnosticConfig.collectRetainingPathForNonGCed) {
// This early check is needed to collect retaing pathes before forced GC,
// because pathes are unavailable for GCed objects.
await checkNonGCed();
}
await _forceGC(
gcCycles: gcCountBuffer,
timeout: timeoutForFinalGarbageCollection,
);
leaks = await collectLeaks();
if (leaks.total > 0 && shouldThrowOnLeaks) {
// `expect` should not be used here, because, when the method is used
// from Flutter, the packages `test` and `flutter_test` conflict.
throw MemoryLeaksDetectedError(leaks);
}
},
);
return leaks;
} finally {
disableLeakTracking();
}
}
/// Forces garbage collection by aggressive memory allocation.
Future<void> _forceGC({required int gcCycles, Duration? timeout}) async {
final start = clock.now();
final barrier = reachabilityBarrier;
final storage = <List<DateTime>>[];
void allocateMemory() {
storage.add(Iterable.generate(10000, (_) => DateTime.now()).toList());
if (storage.length > 100) storage.removeAt(0);
}
while (reachabilityBarrier < barrier + gcCycles) {
if (timeout != null && clock.now().difference(start) > timeout) {
throw TimeoutException('forceGC timed out', timeout);
}
await Future.delayed(const Duration());
allocateMemory();
}
}