Skip to content

Commit bff6b93

Browse files
authored
Next attempt to make inspector weakly referencing the inspected objects. (#129962)
1 parent 4f6c887 commit bff6b93

File tree

5 files changed

+147
-25
lines changed

5 files changed

+147
-25
lines changed

packages/flutter/lib/src/foundation/diagnostics.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3292,6 +3292,8 @@ mixin Diagnosticable {
32923292
/// {@end-tool}
32933293
///
32943294
/// Used by [toDiagnosticsNode] and [toString].
3295+
///
3296+
/// Do not add values, that have lifetime shorter than the object.
32953297
@protected
32963298
@mustCallSuper
32973299
void debugFillProperties(DiagnosticPropertiesBuilder properties) { }

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4794,7 +4794,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
47944794
final List<DiagnosticsNode> diagnosticsDependencies = sortedDependencies
47954795
.map((InheritedElement element) => element.widget.toDiagnosticsNode(style: DiagnosticsTreeStyle.sparse))
47964796
.toList();
4797-
properties.add(DiagnosticsProperty<List<DiagnosticsNode>>('dependencies', diagnosticsDependencies));
4797+
properties.add(DiagnosticsProperty<Set<InheritedElement>>('dependencies', deps, description: diagnosticsDependencies.toString()));
47984798
}
47994799
}
48004800

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

Lines changed: 100 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -680,11 +680,39 @@ typedef InspectorSelectionChangedCallback = void Function();
680680

681681
/// Structure to help reference count Dart objects referenced by a GUI tool
682682
/// using [WidgetInspectorService].
683-
class _InspectorReferenceData {
684-
_InspectorReferenceData(this.object);
683+
///
684+
/// Does not hold the object from garbage collection.
685+
@visibleForTesting
686+
class InspectorReferenceData {
687+
/// Creates an instance of [InspectorReferenceData].
688+
InspectorReferenceData(Object object, this.id) {
689+
// These types are not supported by [WeakReference].
690+
// See https://api.dart.dev/stable/3.0.2/dart-core/WeakReference-class.html
691+
if (object is String || object is num || object is bool) {
692+
_value = object;
693+
return;
694+
}
695+
696+
_ref = WeakReference<Object>(object);
697+
}
698+
699+
WeakReference<Object>? _ref;
700+
701+
Object? _value;
702+
703+
/// The id of the object in the widget inspector records.
704+
final String id;
685705

686-
final Object object;
706+
/// The number of times the object has been referenced.
687707
int count = 1;
708+
709+
/// The value.
710+
Object? get value {
711+
if (_ref != null) {
712+
return _ref!.target;
713+
}
714+
return _value;
715+
}
688716
}
689717

690718
// Production implementation of [WidgetInspectorService].
@@ -742,9 +770,9 @@ mixin WidgetInspectorService {
742770
/// The VM service protocol does not keep alive object references so this
743771
/// class needs to manually manage groups of objects that should be kept
744772
/// alive.
745-
final Map<String, Set<_InspectorReferenceData>> _groups = <String, Set<_InspectorReferenceData>>{};
746-
final Map<String, _InspectorReferenceData> _idToReferenceData = <String, _InspectorReferenceData>{};
747-
final Map<Object, String> _objectToId = Map<Object, String>.identity();
773+
final Map<String, Set<InspectorReferenceData>> _groups = <String, Set<InspectorReferenceData>>{};
774+
final Map<String, InspectorReferenceData> _idToReferenceData = <String, InspectorReferenceData>{};
775+
final WeakMap<Object, String> _objectToId = WeakMap<Object, String>();
748776
int _nextId = 0;
749777

750778
/// The pubRootDirectories that are currently configured for the widget inspector.
@@ -1270,20 +1298,22 @@ mixin WidgetInspectorService {
12701298
/// references from a different group.
12711299
@protected
12721300
void disposeGroup(String name) {
1273-
final Set<_InspectorReferenceData>? references = _groups.remove(name);
1301+
final Set<InspectorReferenceData>? references = _groups.remove(name);
12741302
if (references == null) {
12751303
return;
12761304
}
12771305
references.forEach(_decrementReferenceCount);
12781306
}
12791307

1280-
void _decrementReferenceCount(_InspectorReferenceData reference) {
1308+
void _decrementReferenceCount(InspectorReferenceData reference) {
12811309
reference.count -= 1;
12821310
assert(reference.count >= 0);
12831311
if (reference.count == 0) {
1284-
final String? id = _objectToId.remove(reference.object);
1285-
assert(id != null);
1286-
_idToReferenceData.remove(id);
1312+
final Object? value = reference.value;
1313+
if (value != null) {
1314+
_objectToId.remove(value);
1315+
}
1316+
_idToReferenceData.remove(reference.id);
12871317
}
12881318
}
12891319

@@ -1295,14 +1325,16 @@ mixin WidgetInspectorService {
12951325
return null;
12961326
}
12971327

1298-
final Set<_InspectorReferenceData> group = _groups.putIfAbsent(groupName, () => Set<_InspectorReferenceData>.identity());
1328+
final Set<InspectorReferenceData> group = _groups.putIfAbsent(groupName, () => Set<InspectorReferenceData>.identity());
12991329
String? id = _objectToId[object];
1300-
_InspectorReferenceData referenceData;
1330+
InspectorReferenceData referenceData;
13011331
if (id == null) {
1332+
// TODO(polina-c): comment here why we increase memory footprint by the prefix 'inspector-'.
1333+
// https://github.com/flutter/devtools/issues/5995
13021334
id = 'inspector-$_nextId';
13031335
_nextId += 1;
13041336
_objectToId[object] = id;
1305-
referenceData = _InspectorReferenceData(object);
1337+
referenceData = InspectorReferenceData(object, id);
13061338
_idToReferenceData[id] = referenceData;
13071339
group.add(referenceData);
13081340
} else {
@@ -1332,11 +1364,11 @@ mixin WidgetInspectorService {
13321364
return null;
13331365
}
13341366

1335-
final _InspectorReferenceData? data = _idToReferenceData[id];
1367+
final InspectorReferenceData? data = _idToReferenceData[id];
13361368
if (data == null) {
13371369
throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Id does not exist.')]);
13381370
}
1339-
return data.object;
1371+
return data.value;
13401372
}
13411373

13421374
/// Returns the object to introspect to determine the source location of an
@@ -1368,7 +1400,7 @@ mixin WidgetInspectorService {
13681400
return;
13691401
}
13701402

1371-
final _InspectorReferenceData? referenceData = _idToReferenceData[id];
1403+
final InspectorReferenceData? referenceData = _idToReferenceData[id];
13721404
if (referenceData == null) {
13731405
throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Id does not exist')]);
13741406
}
@@ -3731,3 +3763,54 @@ class _WidgetFactory {
37313763
// recognize the annotation.
37323764
// ignore: library_private_types_in_public_api
37333765
const _WidgetFactory widgetFactory = _WidgetFactory();
3766+
3767+
/// Does not hold keys from garbage collection.
3768+
@visibleForTesting
3769+
class WeakMap<K, V> {
3770+
Expando<Object> _objects = Expando<Object>();
3771+
3772+
/// Strings, numbers, booleans.
3773+
final Map<K, V> _primitives = <K, V>{};
3774+
3775+
bool _isPrimitive(Object? key) {
3776+
return key == null || key is String || key is num || key is bool;
3777+
}
3778+
3779+
/// Returns the value for the given [key] or null if [key] is not in the map
3780+
/// or garbage collected.
3781+
///
3782+
/// Does not support records to act as keys.
3783+
V? operator [](K key){
3784+
if (_isPrimitive(key)) {
3785+
return _primitives[key];
3786+
} else {
3787+
return _objects[key!] as V?;
3788+
}
3789+
}
3790+
3791+
/// Associates the [key] with the given [value].
3792+
void operator []=(K key, V value){
3793+
if (_isPrimitive(key)) {
3794+
_primitives[key] = value;
3795+
} else {
3796+
_objects[key!] = value;
3797+
}
3798+
}
3799+
3800+
/// Removes the value for the given [key] from the map.
3801+
V? remove(K key) {
3802+
if (_isPrimitive(key)) {
3803+
return _primitives.remove(key);
3804+
} else {
3805+
final V? result = _objects[key!] as V?;
3806+
_objects[key] = null;
3807+
return result;
3808+
}
3809+
}
3810+
3811+
/// Removes all pairs from the map.
3812+
void clear() {
3813+
_objects = Expando<Object>();
3814+
_primitives.clear();
3815+
}
3816+
}

packages/flutter/test/widgets/framework_test.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1592,13 +1592,13 @@ void main() {
15921592
builder.properties.any((DiagnosticsNode property) => property.name == 'dependencies' && property.value != null),
15931593
isTrue,
15941594
);
1595-
final DiagnosticsProperty<List<DiagnosticsNode>> dependenciesProperty =
1596-
builder.properties.firstWhere((DiagnosticsNode property) => property.name == 'dependencies') as DiagnosticsProperty<List<DiagnosticsNode>>;
1595+
final DiagnosticsProperty<Set<InheritedElement>> dependenciesProperty =
1596+
builder.properties.firstWhere((DiagnosticsNode property) => property.name == 'dependencies') as DiagnosticsProperty<Set<InheritedElement>>;
15971597
expect(dependenciesProperty, isNotNull);
15981598

1599-
final List<DiagnosticsNode> dependencies = dependenciesProperty.value!;
1599+
final Set<InheritedElement> dependencies = dependenciesProperty.value!;
16001600
expect(dependencies.length, equals(3));
1601-
expect(dependencies.toString(), '[ButtonBarTheme, Directionality, FocusTraversalOrder]');
1601+
expect(dependenciesProperty.toDescription(), '[ButtonBarTheme, Directionality, FocusTraversalOrder]');
16021602
});
16031603

16041604
testWidgets('BuildOwner.globalKeyCount keeps track of in-use global keys', (WidgetTester tester) async {

packages/flutter/test/widgets/widget_inspector_test.dart

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,41 @@ extension TextFromString on String {
240240
}
241241
}
242242

243+
final List<Object> _weakValueTests = <Object>[1, 1.0, 'hello', true, false, Object(), <int>[3, 4], DateTime(2023)];
244+
243245
void main() {
246+
group('$InspectorReferenceData', (){
247+
for (final Object item in _weakValueTests) {
248+
test('can be created for any type but $Record, $item', () async {
249+
final InspectorReferenceData weakValue = InspectorReferenceData(item, 'id');
250+
expect(weakValue.value, item);
251+
});
252+
}
253+
254+
test('throws for $Record', () async {
255+
expect(()=> InspectorReferenceData((1, 2), 'id'), throwsA(isA<ArgumentError>()));
256+
});
257+
});
258+
259+
group('$WeakMap', (){
260+
for (final Object item in _weakValueTests) {
261+
test('assigns and removes value, $item', () async {
262+
final WeakMap<Object, Object> weakMap = WeakMap<Object, Object>();
263+
weakMap[item] = 1;
264+
expect(weakMap[item], 1);
265+
expect(weakMap.remove(item), 1);
266+
expect(weakMap[item], null);
267+
});
268+
}
269+
270+
for (final Object item in _weakValueTests) {
271+
test('returns null for absent value, $item', () async {
272+
final WeakMap<Object, Object> weakMap = WeakMap<Object, Object>();
273+
expect(weakMap[item], null);
274+
});
275+
}
276+
});
277+
244278
_TestWidgetInspectorService.runTests();
245279
}
246280

@@ -2184,7 +2218,8 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
21842218
final List<DiagnosticsNode> expectedProperties = element.toDiagnosticsNode().getProperties();
21852219
final Iterable<Object?> propertyValues = expectedProperties.map((DiagnosticsNode e) => e.value.toString());
21862220
for (final Map<String, Object?> propertyJson in propertiesJson.cast<Map<String, Object?>>()) {
2187-
final String property = service.toObject(propertyJson['valueId']! as String)!.toString();
2221+
final String id = propertyJson['valueId']! as String;
2222+
final String property = service.toObject(id)!.toString();
21882223
expect(propertyValues, contains(property));
21892224
}
21902225
}
@@ -2227,7 +2262,8 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
22272262
final List<DiagnosticsNode> expectedProperties = element.toDiagnosticsNode().getProperties();
22282263
final Iterable<Object?> propertyValues = expectedProperties.map((DiagnosticsNode e) => e.value.toString());
22292264
for (final Map<String, Object?> propertyJson in propertiesJson.cast<Map<String, Object?>>()) {
2230-
final String property = service.toObject(propertyJson['valueId']! as String)!.toString();
2265+
final String id = propertyJson['valueId']! as String;
2266+
final String property = service.toObject(id)!.toString();
22312267
expect(propertyValues, contains(property));
22322268
}
22332269
}
@@ -2283,7 +2319,6 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
22832319

22842320
testWidgets('ext.flutter.inspector.getRootWidgetSummaryTree', (WidgetTester tester) async {
22852321
const String group = 'test-group';
2286-
22872322
await tester.pumpWidget(
22882323
const Directionality(
22892324
textDirection: TextDirection.ltr,
@@ -2296,6 +2331,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
22962331
),
22972332
),
22982333
);
2334+
22992335
final Element elementA = find.text('a').evaluate().first;
23002336

23012337
service.disposeAllGroups();
@@ -2311,6 +2347,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
23112347
WidgetInspectorServiceExtensions.getRootWidgetSummaryTree.name,
23122348
<String, String>{'objectGroup': group},
23132349
))! as Map<String, Object?>;
2350+
23142351
// We haven't yet properly specified which directories are summary tree
23152352
// directories so we get an empty tree other than the root that is always
23162353
// included.

0 commit comments

Comments
 (0)