Unverified Commit f05bb9a1 authored by Kostia Sokolovskyi's avatar Kostia Sokolovskyi Committed by GitHub

Instrument RestorationBucket, _RouteEntry and DisposableBuildContext for leak tracking. (#137477)

parent 50ecd570
......@@ -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();
......
......@@ -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<T extends State> {
///
/// [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<T extends State> {
/// 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;
}
}
......@@ -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<dynamic> 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();
}
......
......@@ -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<String, dynamic> _createRawDataSet() {
......
......@@ -57,6 +57,7 @@ void main() {
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 @@ void main() {
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');
});
......
......@@ -30,6 +30,21 @@ void main() {
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 {
......
......@@ -15,6 +15,7 @@ void main() {
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 @@ 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<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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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(
......
......@@ -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<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 @@ 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<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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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(
......
......@@ -27,6 +27,7 @@ void main() {
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 @@ void main() {
// 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 @@ void main() {
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 @@ void main() {
// 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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
},
};
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 @@ void main() {
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 @@ 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<RestorationBucket>(root);
await tester.pumpWidget(
......@@ -367,7 +376,6 @@ void main() {
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket?>(null);
await tester.pump();
root.dispose();
expect(binding.restorationManager.rootBucketAccessed, 2);
expect(find.text('Hello'), findsOneWidget);
......
......@@ -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<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
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<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
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<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
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<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
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<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
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<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
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<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
context: context,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment