Instrument RestorationBucket, _RouteEntry and DisposableBuildContext for leak tracking. (#137477)
diff --git a/packages/flutter/lib/src/services/restoration.dart b/packages/flutter/lib/src/services/restoration.dart
index b188ec1..a5c37c5 100644
--- a/packages/flutter/lib/src/services/restoration.dart
+++ b/packages/flutter/lib/src/services/restoration.dart
@@ -420,6 +420,12 @@
_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 @@
_debugOwner = debugOwner;
return true;
}());
+ if (kFlutterMemoryAllocationsEnabled) {
+ _maybeDispatchObjectCreation();
+ }
}
/// Creates the root [RestorationBucket] for the provided restoration
@@ -540,6 +549,9 @@
_debugOwner = manager;
return true;
}());
+ if (kFlutterMemoryAllocationsEnabled) {
+ _maybeDispatchObjectCreation();
+ }
}
/// Creates a child bucket initialized with the data that the provided
@@ -563,6 +575,9 @@
_debugOwner = debugOwner;
return true;
}());
+ if (kFlutterMemoryAllocationsEnabled) {
+ _maybeDispatchObjectCreation();
+ }
}
static const String _childrenMapKey = 'c';
@@ -934,6 +949,19 @@
_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 @@
/// 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 f3bcf6a..bc9d869 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 @@
///
/// [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 @@
/// 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 7fc2966..0de4cf1 100644
--- a/packages/flutter/lib/src/widgets/navigator.dart
+++ b/packages/flutter/lib/src/widgets/navigator.dart
@@ -2913,7 +2913,17 @@
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<dynamic> route;
@@ -3125,6 +3135,11 @@
/// 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 49383e4..67058ae 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 @@
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<String, dynamic> _createRawDataSet() {
diff --git a/packages/flutter/test/services/restoration_test.dart b/packages/flutter/test/services/restoration_test.dart
index ce52f5f..413917b 100644
--- a/packages/flutter/test/services/restoration_test.dart
+++ b/packages/flutter/test/services/restoration_test.dart
@@ -57,6 +57,7 @@
expect(rootBucket!.read<int>('value1'), 10);
expect(rootBucket!.read<String>('value2'), 'Hello');
final RestorationBucket child = rootBucket!.claimChild('child1', debugOwner: null);
+ addTearDown(child.dispose);
expect(child.read<int>('another value'), 22);
// Accessing the root bucket again completes synchronously with same bucket.
@@ -157,6 +158,7 @@
expect(newRoot!.read<int>('foo'), 33);
expect(newRoot!.read<int>('value1'), null);
final RestorationBucket newChild = newRoot!.claimChild('childFoo', debugOwner: null);
+ addTearDown(newChild.dispose);
expect(newChild.read<String>('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 c947366..463de7d 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 @@
expect(() => DisposableBuildContext(state), throwsAssertionError);
});
+
+ testWidgetsWithLeakTracking('DisposableBuildContext dispatches memory events', (WidgetTester tester) async {
+ final GlobalKey<TestWidgetState> key = GlobalKey<TestWidgetState>();
+ await tester.pumpWidget(TestWidget(key));
+
+ final TestWidgetState state = key.currentState!;
+
+ await expectLater(
+ await memoryEvents(
+ () => DisposableBuildContext<TestWidgetState>(state).dispose(),
+ DisposableBuildContext<TestWidgetState>,
+ ),
+ 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 7fb55fe..45ddb45 100644
--- a/packages/flutter/test/widgets/restoration_mixin_test.dart
+++ b/packages/flutter/test/widgets/restoration_mixin_test.dart
@@ -15,6 +15,7 @@
addTearDown(manager.dispose);
final Map<String, dynamic> rawData = <String, dynamic>{};
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
+ addTearDown(root.dispose);
expect(rawData, isEmpty);
await tester.pumpWidget(
@@ -41,6 +42,7 @@
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 @@
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 @@
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 @@
addTearDown(manager.dispose);
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
+ addTearDown(root.dispose);
expect((rawData[childrenMapKey] as Map<String, dynamic>).containsKey('child1'), isTrue);
await tester.pumpWidget(
@@ -173,6 +178,7 @@
addTearDown(manager.dispose);
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
+ addTearDown(root.dispose);
await tester.pumpWidget(
UnmanagedRestorationScope(
@@ -235,6 +241,7 @@
addTearDown(manager.dispose);
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
+ addTearDown(root.dispose);
await tester.pumpWidget(
_TestRestorableWidget(
@@ -297,6 +304,7 @@
addTearDown(manager.dispose);
final Map<String, dynamic> rawData = <String, dynamic>{};
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 aaabc66..892b2f7 100644
--- a/packages/flutter/test/widgets/restoration_scope_test.dart
+++ b/packages/flutter/test/widgets/restoration_scope_test.dart
@@ -15,6 +15,7 @@
restorationId: 'foo',
debugOwner: 'owner',
);
+ addTearDown(bucket1.dispose);
await tester.pumpWidget(
UnmanagedRestorationScope(
@@ -31,6 +32,8 @@
restorationId: 'foo2',
debugOwner: 'owner',
);
+ addTearDown(bucket2.dispose);
+
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: bucket2,
@@ -104,6 +107,7 @@
addTearDown(manager.dispose);
final Map<String, dynamic> rawData = <String, dynamic>{};
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
+ addTearDown(root.dispose);
expect(rawData, isEmpty);
await tester.pumpWidget(
@@ -126,6 +130,7 @@
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 @@
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 @@
addTearDown(manager.dispose);
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
+ addTearDown(root.dispose);
expect((rawData[childrenMapKey] as Map<String, dynamic>).containsKey('child1'), isTrue);
await tester.pumpWidget(
@@ -216,6 +223,7 @@
final MockRestorationManager manager = MockRestorationManager();
addTearDown(manager.dispose);
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: <String, dynamic>{});
+ addTearDown(root.dispose);
await tester.pumpWidget(
UnmanagedRestorationScope(
@@ -274,6 +282,8 @@
final MockRestorationManager manager = MockRestorationManager();
addTearDown(manager.dispose);
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: <String, dynamic>{});
+ addTearDown(root.dispose);
+
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
@@ -316,6 +326,7 @@
addTearDown(manager.dispose);
final Map<String, dynamic> rawData = <String, dynamic>{};
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 26db826..07e2e04 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 @@
addTearDown(manager.dispose);
final Map<String, dynamic> rawData = <String, dynamic>{};
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
+ addTearDown(root.dispose);
expect(rawData, isEmpty);
await tester.pumpWidget(
@@ -77,6 +78,7 @@
// Complete the future.
final Map<String, dynamic> rawData = <String, dynamic>{};
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 @@
testWidgetsWithLeakTracking('no delay when root is available synchronously', (WidgetTester tester) async {
final Map<String, dynamic> rawData = <String, dynamic>{};
final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: rawData);
+ addTearDown(root.dispose);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(root);
await tester.pumpWidget(
@@ -156,6 +159,7 @@
// Complete the future.
final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: <String, dynamic>{});
+ addTearDown(root.dispose);
bucketCompleter.complete(root);
await tester.pump(const Duration(milliseconds: 100));
@@ -187,6 +191,7 @@
addTearDown(manager.dispose);
final Map<String, dynamic> inScopeRawData = <String, dynamic>{};
final RestorationBucket inScopeRootBucket = RestorationBucket.root(manager: manager, rawData: inScopeRawData);
+ addTearDown(inScopeRootBucket.dispose);
await tester.pumpWidget(
Directionality(
@@ -231,6 +236,7 @@
final Map<String, dynamic> outOfScopeRawData = <String, dynamic>{};
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 @@
testWidgetsWithLeakTracking('injects new root when old one is decommissioned', (WidgetTester tester) async {
final Map<String, dynamic> firstRawData = <String, dynamic>{};
final RestorationBucket firstRoot = RestorationBucket.root(manager: binding.restorationManager, rawData: firstRawData);
+ addTearDown(firstRoot.dispose);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(firstRoot);
await tester.pumpWidget(
@@ -299,9 +306,9 @@
},
};
final RestorationBucket secondRoot = RestorationBucket.root(manager: binding.restorationManager, rawData: secondRawData);
+ addTearDown(secondRoot.dispose);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(secondRoot);
await tester.pump();
- firstRoot.dispose();
expect(state.bucket, isNot(same(firstBucket)));
expect(state.bucket!.read<int>('foo'), 22);
@@ -336,6 +343,7 @@
expect(state.bucket, isNull);
final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: null);
+ addTearDown(root.dispose);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(root);
await tester.pump();
@@ -346,6 +354,7 @@
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<RestorationBucket>(root);
await tester.pumpWidget(
@@ -367,7 +376,6 @@
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket?>(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 8dcba52..926484b 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 @@
await tester.pumpWidget(TestWidget(key));
final DisposableBuildContext context = DisposableBuildContext(key.currentState!);
+ addTearDown(context.dispose);
final TestImageProvider testImageProvider = TestImageProvider(testImage.clone());
final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
context: context,
@@ -74,6 +75,7 @@
));
final DisposableBuildContext context = DisposableBuildContext(key.currentState!);
+ addTearDown(context.dispose);
final TestImageProvider testImageProvider = TestImageProvider(testImage.clone());
final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
context: context,
@@ -115,6 +117,7 @@
));
final DisposableBuildContext context = DisposableBuildContext(keys.last.currentState!);
+ addTearDown(context.dispose);
final TestImageProvider testImageProvider = TestImageProvider(testImage.clone());
final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
context: context,
@@ -173,6 +176,7 @@
));
final DisposableBuildContext context = DisposableBuildContext(keys.last.currentState!);
+ addTearDown(context.dispose);
final TestImageProvider testImageProvider = TestImageProvider(testImage.clone());
final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
context: context,
@@ -241,6 +245,7 @@
));
final DisposableBuildContext context = DisposableBuildContext(keys.last.currentState!);
+ addTearDown(context.dispose);
final TestImageProvider testImageProvider = TestImageProvider(testImage.clone());
final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
context: context,
@@ -307,6 +312,7 @@
));
final DisposableBuildContext context = DisposableBuildContext(key.currentState!);
+ addTearDown(context.dispose);
final TestImageProvider testImageProvider = TestImageProvider(testImage.clone());
final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
context: context,
@@ -359,6 +365,7 @@
));
final DisposableBuildContext context = DisposableBuildContext(key.currentState!);
+ addTearDown(context.dispose);
final TestImageProvider testImageProvider = TestImageProvider(testImage.clone());
final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
context: context,