Skip to content

Commit f05bb9a

Browse files
authored
Instrument RestorationBucket, _RouteEntry and DisposableBuildContext for leak tracking. (flutter#137477)
1 parent 50ecd57 commit f05bb9a

10 files changed

+166
-4
lines changed

packages/flutter/lib/src/services/restoration.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,12 @@ class RestorationManager extends ChangeNotifier {
420420
_doSerialization();
421421
assert(!_serializationScheduled);
422422
}
423+
424+
@override
425+
void dispose() {
426+
_rootBucket?.dispose();
427+
super.dispose();
428+
}
423429
}
424430

425431
/// A [RestorationBucket] holds pieces of the restoration data that a part of
@@ -507,6 +513,9 @@ class RestorationBucket {
507513
_debugOwner = debugOwner;
508514
return true;
509515
}());
516+
if (kFlutterMemoryAllocationsEnabled) {
517+
_maybeDispatchObjectCreation();
518+
}
510519
}
511520

512521
/// Creates the root [RestorationBucket] for the provided restoration
@@ -540,6 +549,9 @@ class RestorationBucket {
540549
_debugOwner = manager;
541550
return true;
542551
}());
552+
if (kFlutterMemoryAllocationsEnabled) {
553+
_maybeDispatchObjectCreation();
554+
}
543555
}
544556

545557
/// Creates a child bucket initialized with the data that the provided
@@ -563,6 +575,9 @@ class RestorationBucket {
563575
_debugOwner = debugOwner;
564576
return true;
565577
}());
578+
if (kFlutterMemoryAllocationsEnabled) {
579+
_maybeDispatchObjectCreation();
580+
}
566581
}
567582

568583
static const String _childrenMapKey = 'c';
@@ -934,6 +949,19 @@ class RestorationBucket {
934949
_parent?._addChildData(this);
935950
}
936951

952+
// TODO(polina-c): stop duplicating code across disposables
953+
// https://github.com/flutter/flutter/issues/137435
954+
/// Dispatches event of object creation to [MemoryAllocations.instance].
955+
void _maybeDispatchObjectCreation() {
956+
if (kFlutterMemoryAllocationsEnabled) {
957+
MemoryAllocations.instance.dispatchObjectCreated(
958+
library: 'package:flutter/services.dart',
959+
className: '$RestorationBucket',
960+
object: this,
961+
);
962+
}
963+
}
964+
937965
/// Deletes the bucket and all the data stored in it from the bucket
938966
/// hierarchy.
939967
///
@@ -948,6 +976,11 @@ class RestorationBucket {
948976
/// This method must only be called by the object's owner.
949977
void dispose() {
950978
assert(_debugAssertNotDisposed());
979+
// TODO(polina-c): stop duplicating code across disposables
980+
// https://github.com/flutter/flutter/issues/137435
981+
if (kFlutterMemoryAllocationsEnabled) {
982+
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
983+
}
951984
_visitChildren(_dropChild, concurrentModification: true);
952985
_claimedChildren.clear();
953986
_childrenToAdd.clear();

packages/flutter/lib/src/widgets/disposable_build_context.dart

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:flutter/foundation.dart';
6+
57
import 'framework.dart';
68

79
/// Provides non-leaking access to a [BuildContext].
@@ -28,7 +30,17 @@ class DisposableBuildContext<T extends State> {
2830
///
2931
/// [State.mounted] must be true.
3032
DisposableBuildContext(T this._state)
31-
: assert(_state.mounted, 'A DisposableBuildContext was given a BuildContext for an Element that is not mounted.');
33+
: assert(_state.mounted, 'A DisposableBuildContext was given a BuildContext for an Element that is not mounted.') {
34+
// TODO(polina-c): stop duplicating code across disposables
35+
// https://github.com/flutter/flutter/issues/137435
36+
if (kFlutterMemoryAllocationsEnabled) {
37+
MemoryAllocations.instance.dispatchObjectCreated(
38+
library: 'package:flutter/widgets.dart',
39+
className: '$DisposableBuildContext',
40+
object: this,
41+
);
42+
}
43+
}
3244

3345
T? _state;
3446

@@ -66,6 +78,11 @@ class DisposableBuildContext<T extends State> {
6678
/// Creators of this object must call [dispose] when their [Element] is
6779
/// unmounted, i.e. when [State.dispose] is called.
6880
void dispose() {
81+
// TODO(polina-c): stop duplicating code across disposables
82+
// https://github.com/flutter/flutter/issues/137435
83+
if (kFlutterMemoryAllocationsEnabled) {
84+
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
85+
}
6986
_state = null;
7087
}
7188
}

packages/flutter/lib/src/widgets/navigator.dart

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2913,7 +2913,17 @@ class _RouteEntry extends RouteTransitionRecord {
29132913
initialState == _RouteLifecycle.pushReplace ||
29142914
initialState == _RouteLifecycle.replace,
29152915
),
2916-
currentState = initialState;
2916+
currentState = initialState {
2917+
// TODO(polina-c): stop duplicating code across disposables
2918+
// https://github.com/flutter/flutter/issues/137435
2919+
if (kFlutterMemoryAllocationsEnabled) {
2920+
MemoryAllocations.instance.dispatchObjectCreated(
2921+
library: 'package:flutter/widgets.dart',
2922+
className: '$_RouteEntry',
2923+
object: this,
2924+
);
2925+
}
2926+
}
29172927

29182928
@override
29192929
final Route<dynamic> route;
@@ -3125,6 +3135,11 @@ class _RouteEntry extends RouteTransitionRecord {
31253135
/// before disposing.
31263136
void forcedDispose() {
31273137
assert(currentState.index < _RouteLifecycle.disposed.index);
3138+
// TODO(polina-c): stop duplicating code across disposables
3139+
// https://github.com/flutter/flutter/issues/137435
3140+
if (kFlutterMemoryAllocationsEnabled) {
3141+
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
3142+
}
31283143
currentState = _RouteLifecycle.disposed;
31293144
route.dispose();
31303145
}

packages/flutter/test/services/restoration_bucket_test.dart

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import 'package:flutter/foundation.dart';
77
import 'package:flutter/services.dart';
88
import 'package:flutter_test/flutter_test.dart';
9+
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
910

1011
import 'restoration.dart';
1112

@@ -562,6 +563,51 @@ void main() {
562563
expect(() => bucket.rename('bar'), throwsFlutterError);
563564
expect(() => bucket.dispose(), throwsFlutterError);
564565
});
566+
567+
test('$RestorationBucket dispatches memory events', () async {
568+
await expectLater(
569+
await memoryEvents(
570+
() => RestorationBucket.empty(
571+
restorationId: 'child1',
572+
debugOwner: null,
573+
).dispose(),
574+
RestorationBucket,
575+
),
576+
areCreateAndDispose,
577+
);
578+
579+
final MockRestorationManager manager1 = MockRestorationManager();
580+
addTearDown(manager1.dispose);
581+
await expectLater(
582+
await memoryEvents(
583+
() => RestorationBucket.root(
584+
manager: manager1,
585+
rawData: null,
586+
).dispose(),
587+
RestorationBucket,
588+
),
589+
areCreateAndDispose,
590+
);
591+
592+
final MockRestorationManager manager2 = MockRestorationManager();
593+
addTearDown(manager2.dispose);
594+
final RestorationBucket parent = RestorationBucket.root(
595+
manager: manager2,
596+
rawData: _createRawDataSet()
597+
);
598+
addTearDown(parent.dispose);
599+
await expectLater(
600+
await memoryEvents(
601+
() => RestorationBucket.child(
602+
restorationId: 'child1',
603+
parent: parent,
604+
debugOwner: null,
605+
).dispose(),
606+
RestorationBucket,
607+
),
608+
areCreateAndDispose,
609+
);
610+
});
565611
}
566612

567613
Map<String, dynamic> _createRawDataSet() {

packages/flutter/test/services/restoration_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ void main() {
5757
expect(rootBucket!.read<int>('value1'), 10);
5858
expect(rootBucket!.read<String>('value2'), 'Hello');
5959
final RestorationBucket child = rootBucket!.claimChild('child1', debugOwner: null);
60+
addTearDown(child.dispose);
6061
expect(child.read<int>('another value'), 22);
6162

6263
// Accessing the root bucket again completes synchronously with same bucket.
@@ -157,6 +158,7 @@ void main() {
157158
expect(newRoot!.read<int>('foo'), 33);
158159
expect(newRoot!.read<int>('value1'), null);
159160
final RestorationBucket newChild = newRoot!.claimChild('childFoo', debugOwner: null);
161+
addTearDown(newChild.dispose);
160162
expect(newChild.read<String>('bar'), 'Hello');
161163
});
162164

packages/flutter/test/widgets/disposable_build_context_test.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,21 @@ void main() {
3030

3131
expect(() => DisposableBuildContext(state), throwsAssertionError);
3232
});
33+
34+
testWidgetsWithLeakTracking('DisposableBuildContext dispatches memory events', (WidgetTester tester) async {
35+
final GlobalKey<TestWidgetState> key = GlobalKey<TestWidgetState>();
36+
await tester.pumpWidget(TestWidget(key));
37+
38+
final TestWidgetState state = key.currentState!;
39+
40+
await expectLater(
41+
await memoryEvents(
42+
() => DisposableBuildContext<TestWidgetState>(state).dispose(),
43+
DisposableBuildContext<TestWidgetState>,
44+
),
45+
areCreateAndDispose,
46+
);
47+
});
3348
}
3449

3550
class TestWidget extends StatefulWidget {

packages/flutter/test/widgets/restoration_mixin_test.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ void main() {
1515
addTearDown(manager.dispose);
1616
final Map<String, dynamic> rawData = <String, dynamic>{};
1717
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
18+
addTearDown(root.dispose);
1819
expect(rawData, isEmpty);
1920

2021
await tester.pumpWidget(
@@ -41,6 +42,7 @@ void main() {
4142
final MockRestorationManager manager = MockRestorationManager();
4243
addTearDown(manager.dispose);
4344
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet());
45+
addTearDown(root.dispose);
4446

4547
await tester.pumpWidget(
4648
UnmanagedRestorationScope(
@@ -64,6 +66,7 @@ void main() {
6466
final MockRestorationManager manager = MockRestorationManager();
6567
addTearDown(manager.dispose);
6668
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet());
69+
addTearDown(root.dispose);
6770

6871
await tester.pumpWidget(
6972
UnmanagedRestorationScope(
@@ -107,6 +110,7 @@ void main() {
107110
final MockRestorationManager manager = MockRestorationManager();
108111
addTearDown(manager.dispose);
109112
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet());
113+
addTearDown(root.dispose);
110114

111115
await tester.pumpWidget(
112116
UnmanagedRestorationScope(
@@ -144,6 +148,7 @@ void main() {
144148
addTearDown(manager.dispose);
145149
final Map<String, dynamic> rawData = _createRawDataSet();
146150
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
151+
addTearDown(root.dispose);
147152

148153
expect((rawData[childrenMapKey] as Map<String, dynamic>).containsKey('child1'), isTrue);
149154
await tester.pumpWidget(
@@ -173,6 +178,7 @@ void main() {
173178
addTearDown(manager.dispose);
174179
final Map<String, dynamic> rawData = _createRawDataSet();
175180
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
181+
addTearDown(root.dispose);
176182

177183
await tester.pumpWidget(
178184
UnmanagedRestorationScope(
@@ -235,6 +241,7 @@ void main() {
235241
addTearDown(manager.dispose);
236242
final Map<String, dynamic> rawData = _createRawDataSet();
237243
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
244+
addTearDown(root.dispose);
238245

239246
await tester.pumpWidget(
240247
_TestRestorableWidget(
@@ -297,6 +304,7 @@ void main() {
297304
addTearDown(manager.dispose);
298305
final Map<String, dynamic> rawData = <String, dynamic>{};
299306
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
307+
addTearDown(root.dispose);
300308
final Key key = GlobalKey();
301309

302310
await tester.pumpWidget(

packages/flutter/test/widgets/restoration_scope_test.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ void main() {
1515
restorationId: 'foo',
1616
debugOwner: 'owner',
1717
);
18+
addTearDown(bucket1.dispose);
1819

1920
await tester.pumpWidget(
2021
UnmanagedRestorationScope(
@@ -31,6 +32,8 @@ void main() {
3132
restorationId: 'foo2',
3233
debugOwner: 'owner',
3334
);
35+
addTearDown(bucket2.dispose);
36+
3437
await tester.pumpWidget(
3538
UnmanagedRestorationScope(
3639
bucket: bucket2,
@@ -104,6 +107,7 @@ void main() {
104107
addTearDown(manager.dispose);
105108
final Map<String, dynamic> rawData = <String, dynamic>{};
106109
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
110+
addTearDown(root.dispose);
107111
expect(rawData, isEmpty);
108112

109113
await tester.pumpWidget(
@@ -126,6 +130,7 @@ void main() {
126130
final MockRestorationManager manager = MockRestorationManager();
127131
addTearDown(manager.dispose);
128132
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet());
133+
addTearDown(root.dispose);
129134

130135
await tester.pumpWidget(
131136
UnmanagedRestorationScope(
@@ -147,6 +152,7 @@ void main() {
147152
final MockRestorationManager manager = MockRestorationManager();
148153
addTearDown(manager.dispose);
149154
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet());
155+
addTearDown(root.dispose);
150156

151157
await tester.pumpWidget(
152158
UnmanagedRestorationScope(
@@ -187,6 +193,7 @@ void main() {
187193
addTearDown(manager.dispose);
188194
final Map<String, dynamic> rawData = _createRawDataSet();
189195
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
196+
addTearDown(root.dispose);
190197

191198
expect((rawData[childrenMapKey] as Map<String, dynamic>).containsKey('child1'), isTrue);
192199
await tester.pumpWidget(
@@ -216,6 +223,7 @@ void main() {
216223
final MockRestorationManager manager = MockRestorationManager();
217224
addTearDown(manager.dispose);
218225
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: <String, dynamic>{});
226+
addTearDown(root.dispose);
219227

220228
await tester.pumpWidget(
221229
UnmanagedRestorationScope(
@@ -274,6 +282,8 @@ void main() {
274282
final MockRestorationManager manager = MockRestorationManager();
275283
addTearDown(manager.dispose);
276284
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: <String, dynamic>{});
285+
addTearDown(root.dispose);
286+
277287
await tester.pumpWidget(
278288
UnmanagedRestorationScope(
279289
bucket: root,
@@ -316,6 +326,7 @@ void main() {
316326
addTearDown(manager.dispose);
317327
final Map<String, dynamic> rawData = <String, dynamic>{};
318328
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
329+
addTearDown(root.dispose);
319330
final Key scopeKey = GlobalKey();
320331

321332
await tester.pumpWidget(

0 commit comments

Comments
 (0)