From 685c0ae49c34dd89f4d4dfd84f3e268086243d78 Mon Sep 17 00:00:00 2001 From: Sergey Penkovsky Date: Mon, 13 Oct 2025 17:27:56 +0300 Subject: [PATCH] fix(scope): properly clear binding and module references on dispose Add memory leak/finalizer test to ensure no strong references remain after closing and disposing a scope. --- cherrypick/lib/src/scope.dart | 5 ++ cherrypick/test/binding_memory_leak_test.dart | 66 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 cherrypick/test/binding_memory_leak_test.dart diff --git a/cherrypick/lib/src/scope.dart b/cherrypick/lib/src/scope.dart index 608f72d..cba58c8 100644 --- a/cherrypick/lib/src/scope.dart +++ b/cherrypick/lib/src/scope.dart @@ -498,5 +498,10 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { await d.dispose(); } _disposables.clear(); + + // Clear modules + _modulesList.clear(); + // Clear binding-index + _bindingResolvers.clear(); } } diff --git a/cherrypick/test/binding_memory_leak_test.dart b/cherrypick/test/binding_memory_leak_test.dart new file mode 100644 index 0000000..7f74d62 --- /dev/null +++ b/cherrypick/test/binding_memory_leak_test.dart @@ -0,0 +1,66 @@ +import 'dart:async'; +import 'dart:developer'; +import 'package:cherrypick/cherrypick.dart'; +import 'package:test/test.dart'; + +class HeavyService implements Disposable { + static int instanceCount = 0; + HeavyService() { + instanceCount++; + print('HeavyService created. Instance count: ' + '\u001b[32m$instanceCount\u001b[0m'); + } + + @override + void dispose() { + instanceCount--; + print('HeavyService disposed. Instance count: ' + '\u001b[31m$instanceCount\u001b[0m'); + } + + static final Finalizer _finalizer = Finalizer((msg) { + print('GC FINALIZED HeavyService: $msg'); + }); + void registerFinalizer() => _finalizer.attach(this, toString(), detach: this); +} + +class HeavyModule extends Module { + @override + void builder(Scope scope) { + bind().toProvide(() => HeavyService()); + } +} + +void main() { + test('Binding memory is cleared after closing and reopening scope', () async { + final root = CherryPick.openRootScope(); + for (int i = 0; i < 10; i++) { + print('\nIteration $i -------------------------------'); + final subScope = root.openSubScope('leak-test-scope'); + subScope.installModules([HeavyModule()]); + final service = subScope.resolve(); + expect(service, isNotNull); + await root.closeSubScope('leak-test-scope'); + // Dart GC не сразу удаляет освобождённые объекты, добавляем паузу и вызываем GC. + await Future.delayed(const Duration(milliseconds: 200)); + } + + // Если dispose не вызвался, instanceCount > 0 => утечка. + expect(HeavyService.instanceCount, equals(0)); + }); + + test('Service is finalized after scope is closed/cleaned', () async { + final root = CherryPick.openRootScope(); + HeavyService? ref; + { + final sub = root.openSubScope('s'); + sub.installModules([HeavyModule()]); + ref = sub.resolve(); + ref.registerFinalizer(); + expect(HeavyService.instanceCount, 1); + await root.closeSubScope('s'); + } + await Future.delayed(const Duration(seconds: 2)); + expect(HeavyService.instanceCount, 0); + }); +}