diff --git a/packages/flutter/lib/src/services/restoration.dart b/packages/flutter/lib/src/services/restoration.dart index b188ec1afff..a5c37c58ef8 100644 --- a/packages/flutter/lib/src/services/restoration.dart +++ b/packages/flutter/lib/src/services/restoration.dart @@ -420,6 +420,12 @@ class RestorationManager extends ChangeNotifier { _doSerialization(); assert(!_serializationScheduled); } + + @override + void dispose() { + _rootBucket?.dispose(); + super.dispose(); + } } /// A [RestorationBucket] holds pieces of the restoration data that a part of @@ -507,6 +513,9 @@ class RestorationBucket { _debugOwner = debugOwner; return true; }()); + if (kFlutterMemoryAllocationsEnabled) { + _maybeDispatchObjectCreation(); + } } /// Creates the root [RestorationBucket] for the provided restoration @@ -540,6 +549,9 @@ class RestorationBucket { _debugOwner = manager; return true; }()); + if (kFlutterMemoryAllocationsEnabled) { + _maybeDispatchObjectCreation(); + } } /// Creates a child bucket initialized with the data that the provided @@ -563,6 +575,9 @@ class RestorationBucket { _debugOwner = debugOwner; return true; }()); + if (kFlutterMemoryAllocationsEnabled) { + _maybeDispatchObjectCreation(); + } } static const String _childrenMapKey = 'c'; @@ -934,6 +949,19 @@ class RestorationBucket { _parent?._addChildData(this); } + // TODO(polina-c): stop duplicating code across disposables + // https://github.com/flutter/flutter/issues/137435 + /// Dispatches event of object creation to [MemoryAllocations.instance]. + void _maybeDispatchObjectCreation() { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectCreated( + library: 'package:flutter/services.dart', + className: '$RestorationBucket', + object: this, + ); + } + } + /// Deletes the bucket and all the data stored in it from the bucket /// hierarchy. /// @@ -948,6 +976,11 @@ class RestorationBucket { /// This method must only be called by the object's owner. void dispose() { assert(_debugAssertNotDisposed()); + // TODO(polina-c): stop duplicating code across disposables + // https://github.com/flutter/flutter/issues/137435 + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectDisposed(object: this); + } _visitChildren(_dropChild, concurrentModification: true); _claimedChildren.clear(); _childrenToAdd.clear(); diff --git a/packages/flutter/lib/src/widgets/disposable_build_context.dart b/packages/flutter/lib/src/widgets/disposable_build_context.dart index f3bcf6abd7f..bc9d869fec4 100644 --- a/packages/flutter/lib/src/widgets/disposable_build_context.dart +++ b/packages/flutter/lib/src/widgets/disposable_build_context.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; + import 'framework.dart'; /// Provides non-leaking access to a [BuildContext]. @@ -28,7 +30,17 @@ class DisposableBuildContext { /// /// [State.mounted] must be true. DisposableBuildContext(T this._state) - : assert(_state.mounted, 'A DisposableBuildContext was given a BuildContext for an Element that is not mounted.'); + : assert(_state.mounted, 'A DisposableBuildContext was given a BuildContext for an Element that is not mounted.') { + // TODO(polina-c): stop duplicating code across disposables + // https://github.com/flutter/flutter/issues/137435 + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectCreated( + library: 'package:flutter/widgets.dart', + className: '$DisposableBuildContext', + object: this, + ); + } + } T? _state; @@ -66,6 +78,11 @@ class DisposableBuildContext { /// Creators of this object must call [dispose] when their [Element] is /// unmounted, i.e. when [State.dispose] is called. void dispose() { + // TODO(polina-c): stop duplicating code across disposables + // https://github.com/flutter/flutter/issues/137435 + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectDisposed(object: this); + } _state = null; } } diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index 7fc29669c14..0de4cf1832c 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -2913,7 +2913,17 @@ class _RouteEntry extends RouteTransitionRecord { initialState == _RouteLifecycle.pushReplace || initialState == _RouteLifecycle.replace, ), - currentState = initialState; + currentState = initialState { + // TODO(polina-c): stop duplicating code across disposables + // https://github.com/flutter/flutter/issues/137435 + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectCreated( + library: 'package:flutter/widgets.dart', + className: '$_RouteEntry', + object: this, + ); + } + } @override final Route route; @@ -3125,6 +3135,11 @@ class _RouteEntry extends RouteTransitionRecord { /// before disposing. void forcedDispose() { assert(currentState.index < _RouteLifecycle.disposed.index); + // TODO(polina-c): stop duplicating code across disposables + // https://github.com/flutter/flutter/issues/137435 + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectDisposed(object: this); + } currentState = _RouteLifecycle.disposed; route.dispose(); } diff --git a/packages/flutter/test/services/restoration_bucket_test.dart b/packages/flutter/test/services/restoration_bucket_test.dart index 49383e41f87..67058aeb79b 100644 --- a/packages/flutter/test/services/restoration_bucket_test.dart +++ b/packages/flutter/test/services/restoration_bucket_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'restoration.dart'; @@ -562,6 +563,51 @@ void main() { expect(() => bucket.rename('bar'), throwsFlutterError); expect(() => bucket.dispose(), throwsFlutterError); }); + + test('$RestorationBucket dispatches memory events', () async { + await expectLater( + await memoryEvents( + () => RestorationBucket.empty( + restorationId: 'child1', + debugOwner: null, + ).dispose(), + RestorationBucket, + ), + areCreateAndDispose, + ); + + final MockRestorationManager manager1 = MockRestorationManager(); + addTearDown(manager1.dispose); + await expectLater( + await memoryEvents( + () => RestorationBucket.root( + manager: manager1, + rawData: null, + ).dispose(), + RestorationBucket, + ), + areCreateAndDispose, + ); + + final MockRestorationManager manager2 = MockRestorationManager(); + addTearDown(manager2.dispose); + final RestorationBucket parent = RestorationBucket.root( + manager: manager2, + rawData: _createRawDataSet() + ); + addTearDown(parent.dispose); + await expectLater( + await memoryEvents( + () => RestorationBucket.child( + restorationId: 'child1', + parent: parent, + debugOwner: null, + ).dispose(), + RestorationBucket, + ), + areCreateAndDispose, + ); + }); } Map _createRawDataSet() { diff --git a/packages/flutter/test/services/restoration_test.dart b/packages/flutter/test/services/restoration_test.dart index ce52f5f342b..413917b5a7e 100644 --- a/packages/flutter/test/services/restoration_test.dart +++ b/packages/flutter/test/services/restoration_test.dart @@ -57,6 +57,7 @@ void main() { expect(rootBucket!.read('value1'), 10); expect(rootBucket!.read('value2'), 'Hello'); final RestorationBucket child = rootBucket!.claimChild('child1', debugOwner: null); + addTearDown(child.dispose); expect(child.read('another value'), 22); // Accessing the root bucket again completes synchronously with same bucket. @@ -157,6 +158,7 @@ void main() { expect(newRoot!.read('foo'), 33); expect(newRoot!.read('value1'), null); final RestorationBucket newChild = newRoot!.claimChild('childFoo', debugOwner: null); + addTearDown(newChild.dispose); expect(newChild.read('bar'), 'Hello'); }); diff --git a/packages/flutter/test/widgets/disposable_build_context_test.dart b/packages/flutter/test/widgets/disposable_build_context_test.dart index c9473665dc4..463de7d7871 100644 --- a/packages/flutter/test/widgets/disposable_build_context_test.dart +++ b/packages/flutter/test/widgets/disposable_build_context_test.dart @@ -30,6 +30,21 @@ void main() { expect(() => DisposableBuildContext(state), throwsAssertionError); }); + + testWidgetsWithLeakTracking('DisposableBuildContext dispatches memory events', (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + await tester.pumpWidget(TestWidget(key)); + + final TestWidgetState state = key.currentState!; + + await expectLater( + await memoryEvents( + () => DisposableBuildContext(state).dispose(), + DisposableBuildContext, + ), + areCreateAndDispose, + ); + }); } class TestWidget extends StatefulWidget { diff --git a/packages/flutter/test/widgets/restoration_mixin_test.dart b/packages/flutter/test/widgets/restoration_mixin_test.dart index 7fb55feb461..45ddb451b07 100644 --- a/packages/flutter/test/widgets/restoration_mixin_test.dart +++ b/packages/flutter/test/widgets/restoration_mixin_test.dart @@ -15,6 +15,7 @@ void main() { addTearDown(manager.dispose); final Map rawData = {}; final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData); + addTearDown(root.dispose); expect(rawData, isEmpty); await tester.pumpWidget( @@ -41,6 +42,7 @@ void main() { final MockRestorationManager manager = MockRestorationManager(); addTearDown(manager.dispose); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet()); + addTearDown(root.dispose); await tester.pumpWidget( UnmanagedRestorationScope( @@ -64,6 +66,7 @@ void main() { final MockRestorationManager manager = MockRestorationManager(); addTearDown(manager.dispose); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet()); + addTearDown(root.dispose); await tester.pumpWidget( UnmanagedRestorationScope( @@ -107,6 +110,7 @@ void main() { final MockRestorationManager manager = MockRestorationManager(); addTearDown(manager.dispose); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet()); + addTearDown(root.dispose); await tester.pumpWidget( UnmanagedRestorationScope( @@ -144,6 +148,7 @@ void main() { addTearDown(manager.dispose); final Map rawData = _createRawDataSet(); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData); + addTearDown(root.dispose); expect((rawData[childrenMapKey] as Map).containsKey('child1'), isTrue); await tester.pumpWidget( @@ -173,6 +178,7 @@ void main() { addTearDown(manager.dispose); final Map rawData = _createRawDataSet(); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData); + addTearDown(root.dispose); await tester.pumpWidget( UnmanagedRestorationScope( @@ -235,6 +241,7 @@ void main() { addTearDown(manager.dispose); final Map rawData = _createRawDataSet(); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData); + addTearDown(root.dispose); await tester.pumpWidget( _TestRestorableWidget( @@ -297,6 +304,7 @@ void main() { addTearDown(manager.dispose); final Map rawData = {}; final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData); + addTearDown(root.dispose); final Key key = GlobalKey(); await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/restoration_scope_test.dart b/packages/flutter/test/widgets/restoration_scope_test.dart index aaabc66f6de..892b2f7a508 100644 --- a/packages/flutter/test/widgets/restoration_scope_test.dart +++ b/packages/flutter/test/widgets/restoration_scope_test.dart @@ -15,6 +15,7 @@ void main() { restorationId: 'foo', debugOwner: 'owner', ); + addTearDown(bucket1.dispose); await tester.pumpWidget( UnmanagedRestorationScope( @@ -31,6 +32,8 @@ void main() { restorationId: 'foo2', debugOwner: 'owner', ); + addTearDown(bucket2.dispose); + await tester.pumpWidget( UnmanagedRestorationScope( bucket: bucket2, @@ -104,6 +107,7 @@ void main() { addTearDown(manager.dispose); final Map rawData = {}; final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData); + addTearDown(root.dispose); expect(rawData, isEmpty); await tester.pumpWidget( @@ -126,6 +130,7 @@ void main() { final MockRestorationManager manager = MockRestorationManager(); addTearDown(manager.dispose); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet()); + addTearDown(root.dispose); await tester.pumpWidget( UnmanagedRestorationScope( @@ -147,6 +152,7 @@ void main() { final MockRestorationManager manager = MockRestorationManager(); addTearDown(manager.dispose); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet()); + addTearDown(root.dispose); await tester.pumpWidget( UnmanagedRestorationScope( @@ -187,6 +193,7 @@ void main() { addTearDown(manager.dispose); final Map rawData = _createRawDataSet(); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData); + addTearDown(root.dispose); expect((rawData[childrenMapKey] as Map).containsKey('child1'), isTrue); await tester.pumpWidget( @@ -216,6 +223,7 @@ void main() { final MockRestorationManager manager = MockRestorationManager(); addTearDown(manager.dispose); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: {}); + addTearDown(root.dispose); await tester.pumpWidget( UnmanagedRestorationScope( @@ -274,6 +282,8 @@ void main() { final MockRestorationManager manager = MockRestorationManager(); addTearDown(manager.dispose); final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: {}); + addTearDown(root.dispose); + await tester.pumpWidget( UnmanagedRestorationScope( bucket: root, @@ -316,6 +326,7 @@ void main() { addTearDown(manager.dispose); final Map rawData = {}; final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData); + addTearDown(root.dispose); final Key scopeKey = GlobalKey(); await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/root_restoration_scope_test.dart b/packages/flutter/test/widgets/root_restoration_scope_test.dart index 26db826c53a..07e2e046f2a 100644 --- a/packages/flutter/test/widgets/root_restoration_scope_test.dart +++ b/packages/flutter/test/widgets/root_restoration_scope_test.dart @@ -27,6 +27,7 @@ void main() { addTearDown(manager.dispose); final Map rawData = {}; final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData); + addTearDown(root.dispose); expect(rawData, isEmpty); await tester.pumpWidget( @@ -77,6 +78,7 @@ void main() { // Complete the future. final Map rawData = {}; final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: rawData); + addTearDown(root.dispose); bucketCompleter.complete(root); await tester.pump(const Duration(milliseconds: 100)); @@ -92,6 +94,7 @@ void main() { testWidgetsWithLeakTracking('no delay when root is available synchronously', (WidgetTester tester) async { final Map rawData = {}; final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: rawData); + addTearDown(root.dispose); binding.restorationManager.rootBucket = SynchronousFuture(root); await tester.pumpWidget( @@ -156,6 +159,7 @@ void main() { // Complete the future. final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: {}); + addTearDown(root.dispose); bucketCompleter.complete(root); await tester.pump(const Duration(milliseconds: 100)); @@ -187,6 +191,7 @@ void main() { addTearDown(manager.dispose); final Map inScopeRawData = {}; final RestorationBucket inScopeRootBucket = RestorationBucket.root(manager: manager, rawData: inScopeRawData); + addTearDown(inScopeRootBucket.dispose); await tester.pumpWidget( Directionality( @@ -231,6 +236,7 @@ void main() { final Map outOfScopeRawData = {}; final RestorationBucket outOfScopeRootBucket = RestorationBucket.root(manager: binding.restorationManager, rawData: outOfScopeRawData); + addTearDown(outOfScopeRootBucket.dispose); bucketCompleter.complete(outOfScopeRootBucket); await tester.pump(const Duration(milliseconds: 100)); @@ -267,6 +273,7 @@ void main() { testWidgetsWithLeakTracking('injects new root when old one is decommissioned', (WidgetTester tester) async { final Map firstRawData = {}; final RestorationBucket firstRoot = RestorationBucket.root(manager: binding.restorationManager, rawData: firstRawData); + addTearDown(firstRoot.dispose); binding.restorationManager.rootBucket = SynchronousFuture(firstRoot); await tester.pumpWidget( @@ -299,9 +306,9 @@ void main() { }, }; final RestorationBucket secondRoot = RestorationBucket.root(manager: binding.restorationManager, rawData: secondRawData); + addTearDown(secondRoot.dispose); binding.restorationManager.rootBucket = SynchronousFuture(secondRoot); await tester.pump(); - firstRoot.dispose(); expect(state.bucket, isNot(same(firstBucket))); expect(state.bucket!.read('foo'), 22); @@ -336,6 +343,7 @@ void main() { expect(state.bucket, isNull); final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: null); + addTearDown(root.dispose); binding.restorationManager.rootBucket = SynchronousFuture(root); await tester.pump(); @@ -346,6 +354,7 @@ void main() { testWidgetsWithLeakTracking('can switch to null', (WidgetTester tester) async { final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: null); + addTearDown(root.dispose); binding.restorationManager.rootBucket = SynchronousFuture(root); await tester.pumpWidget( @@ -367,7 +376,6 @@ void main() { binding.restorationManager.rootBucket = SynchronousFuture(null); await tester.pump(); - root.dispose(); expect(binding.restorationManager.rootBucketAccessed, 2); expect(find.text('Hello'), findsOneWidget); diff --git a/packages/flutter/test/widgets/scroll_aware_image_provider_test.dart b/packages/flutter/test/widgets/scroll_aware_image_provider_test.dart index 8dcba52ef92..926484b5993 100644 --- a/packages/flutter/test/widgets/scroll_aware_image_provider_test.dart +++ b/packages/flutter/test/widgets/scroll_aware_image_provider_test.dart @@ -39,6 +39,7 @@ void main() { await tester.pumpWidget(TestWidget(key)); final DisposableBuildContext context = DisposableBuildContext(key.currentState!); + addTearDown(context.dispose); final TestImageProvider testImageProvider = TestImageProvider(testImage.clone()); final ScrollAwareImageProvider imageProvider = ScrollAwareImageProvider( context: context, @@ -74,6 +75,7 @@ void main() { )); final DisposableBuildContext context = DisposableBuildContext(key.currentState!); + addTearDown(context.dispose); final TestImageProvider testImageProvider = TestImageProvider(testImage.clone()); final ScrollAwareImageProvider imageProvider = ScrollAwareImageProvider( context: context, @@ -115,6 +117,7 @@ void main() { )); final DisposableBuildContext context = DisposableBuildContext(keys.last.currentState!); + addTearDown(context.dispose); final TestImageProvider testImageProvider = TestImageProvider(testImage.clone()); final ScrollAwareImageProvider imageProvider = ScrollAwareImageProvider( context: context, @@ -173,6 +176,7 @@ void main() { )); final DisposableBuildContext context = DisposableBuildContext(keys.last.currentState!); + addTearDown(context.dispose); final TestImageProvider testImageProvider = TestImageProvider(testImage.clone()); final ScrollAwareImageProvider imageProvider = ScrollAwareImageProvider( context: context, @@ -241,6 +245,7 @@ void main() { )); final DisposableBuildContext context = DisposableBuildContext(keys.last.currentState!); + addTearDown(context.dispose); final TestImageProvider testImageProvider = TestImageProvider(testImage.clone()); final ScrollAwareImageProvider imageProvider = ScrollAwareImageProvider( context: context, @@ -307,6 +312,7 @@ void main() { )); final DisposableBuildContext context = DisposableBuildContext(key.currentState!); + addTearDown(context.dispose); final TestImageProvider testImageProvider = TestImageProvider(testImage.clone()); final ScrollAwareImageProvider imageProvider = ScrollAwareImageProvider( context: context, @@ -359,6 +365,7 @@ void main() { )); final DisposableBuildContext context = DisposableBuildContext(key.currentState!); + addTearDown(context.dispose); final TestImageProvider testImageProvider = TestImageProvider(testImage.clone()); final ScrollAwareImageProvider imageProvider = ScrollAwareImageProvider( context: context,