Unverified Commit bff6b936 authored by Polina Cherkasova's avatar Polina Cherkasova Committed by GitHub

Next attempt to make inspector weakly referencing the inspected objects. (#129962)

parent 4f6c8877
...@@ -3292,6 +3292,8 @@ mixin Diagnosticable { ...@@ -3292,6 +3292,8 @@ mixin Diagnosticable {
/// {@end-tool} /// {@end-tool}
/// ///
/// Used by [toDiagnosticsNode] and [toString]. /// Used by [toDiagnosticsNode] and [toString].
///
/// Do not add values, that have lifetime shorter than the object.
@protected @protected
@mustCallSuper @mustCallSuper
void debugFillProperties(DiagnosticPropertiesBuilder properties) { } void debugFillProperties(DiagnosticPropertiesBuilder properties) { }
......
...@@ -4794,7 +4794,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -4794,7 +4794,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
final List<DiagnosticsNode> diagnosticsDependencies = sortedDependencies final List<DiagnosticsNode> diagnosticsDependencies = sortedDependencies
.map((InheritedElement element) => element.widget.toDiagnosticsNode(style: DiagnosticsTreeStyle.sparse)) .map((InheritedElement element) => element.widget.toDiagnosticsNode(style: DiagnosticsTreeStyle.sparse))
.toList(); .toList();
properties.add(DiagnosticsProperty<List<DiagnosticsNode>>('dependencies', diagnosticsDependencies)); properties.add(DiagnosticsProperty<Set<InheritedElement>>('dependencies', deps, description: diagnosticsDependencies.toString()));
} }
} }
......
...@@ -680,11 +680,39 @@ typedef InspectorSelectionChangedCallback = void Function(); ...@@ -680,11 +680,39 @@ typedef InspectorSelectionChangedCallback = void Function();
/// Structure to help reference count Dart objects referenced by a GUI tool /// Structure to help reference count Dart objects referenced by a GUI tool
/// using [WidgetInspectorService]. /// using [WidgetInspectorService].
class _InspectorReferenceData { ///
_InspectorReferenceData(this.object); /// Does not hold the object from garbage collection.
@visibleForTesting
class InspectorReferenceData {
/// Creates an instance of [InspectorReferenceData].
InspectorReferenceData(Object object, this.id) {
// These types are not supported by [WeakReference].
// See https://api.dart.dev/stable/3.0.2/dart-core/WeakReference-class.html
if (object is String || object is num || object is bool) {
_value = object;
return;
}
_ref = WeakReference<Object>(object);
}
WeakReference<Object>? _ref;
Object? _value;
/// The id of the object in the widget inspector records.
final String id;
final Object object; /// The number of times the object has been referenced.
int count = 1; int count = 1;
/// The value.
Object? get value {
if (_ref != null) {
return _ref!.target;
}
return _value;
}
} }
// Production implementation of [WidgetInspectorService]. // Production implementation of [WidgetInspectorService].
...@@ -742,9 +770,9 @@ mixin WidgetInspectorService { ...@@ -742,9 +770,9 @@ mixin WidgetInspectorService {
/// The VM service protocol does not keep alive object references so this /// The VM service protocol does not keep alive object references so this
/// class needs to manually manage groups of objects that should be kept /// class needs to manually manage groups of objects that should be kept
/// alive. /// alive.
final Map<String, Set<_InspectorReferenceData>> _groups = <String, Set<_InspectorReferenceData>>{}; final Map<String, Set<InspectorReferenceData>> _groups = <String, Set<InspectorReferenceData>>{};
final Map<String, _InspectorReferenceData> _idToReferenceData = <String, _InspectorReferenceData>{}; final Map<String, InspectorReferenceData> _idToReferenceData = <String, InspectorReferenceData>{};
final Map<Object, String> _objectToId = Map<Object, String>.identity(); final WeakMap<Object, String> _objectToId = WeakMap<Object, String>();
int _nextId = 0; int _nextId = 0;
/// The pubRootDirectories that are currently configured for the widget inspector. /// The pubRootDirectories that are currently configured for the widget inspector.
...@@ -1270,20 +1298,22 @@ mixin WidgetInspectorService { ...@@ -1270,20 +1298,22 @@ mixin WidgetInspectorService {
/// references from a different group. /// references from a different group.
@protected @protected
void disposeGroup(String name) { void disposeGroup(String name) {
final Set<_InspectorReferenceData>? references = _groups.remove(name); final Set<InspectorReferenceData>? references = _groups.remove(name);
if (references == null) { if (references == null) {
return; return;
} }
references.forEach(_decrementReferenceCount); references.forEach(_decrementReferenceCount);
} }
void _decrementReferenceCount(_InspectorReferenceData reference) { void _decrementReferenceCount(InspectorReferenceData reference) {
reference.count -= 1; reference.count -= 1;
assert(reference.count >= 0); assert(reference.count >= 0);
if (reference.count == 0) { if (reference.count == 0) {
final String? id = _objectToId.remove(reference.object); final Object? value = reference.value;
assert(id != null); if (value != null) {
_idToReferenceData.remove(id); _objectToId.remove(value);
}
_idToReferenceData.remove(reference.id);
} }
} }
...@@ -1295,14 +1325,16 @@ mixin WidgetInspectorService { ...@@ -1295,14 +1325,16 @@ mixin WidgetInspectorService {
return null; return null;
} }
final Set<_InspectorReferenceData> group = _groups.putIfAbsent(groupName, () => Set<_InspectorReferenceData>.identity()); final Set<InspectorReferenceData> group = _groups.putIfAbsent(groupName, () => Set<InspectorReferenceData>.identity());
String? id = _objectToId[object]; String? id = _objectToId[object];
_InspectorReferenceData referenceData; InspectorReferenceData referenceData;
if (id == null) { if (id == null) {
// TODO(polina-c): comment here why we increase memory footprint by the prefix 'inspector-'.
// https://github.com/flutter/devtools/issues/5995
id = 'inspector-$_nextId'; id = 'inspector-$_nextId';
_nextId += 1; _nextId += 1;
_objectToId[object] = id; _objectToId[object] = id;
referenceData = _InspectorReferenceData(object); referenceData = InspectorReferenceData(object, id);
_idToReferenceData[id] = referenceData; _idToReferenceData[id] = referenceData;
group.add(referenceData); group.add(referenceData);
} else { } else {
...@@ -1332,11 +1364,11 @@ mixin WidgetInspectorService { ...@@ -1332,11 +1364,11 @@ mixin WidgetInspectorService {
return null; return null;
} }
final _InspectorReferenceData? data = _idToReferenceData[id]; final InspectorReferenceData? data = _idToReferenceData[id];
if (data == null) { if (data == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Id does not exist.')]); throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Id does not exist.')]);
} }
return data.object; return data.value;
} }
/// Returns the object to introspect to determine the source location of an /// Returns the object to introspect to determine the source location of an
...@@ -1368,7 +1400,7 @@ mixin WidgetInspectorService { ...@@ -1368,7 +1400,7 @@ mixin WidgetInspectorService {
return; return;
} }
final _InspectorReferenceData? referenceData = _idToReferenceData[id]; final InspectorReferenceData? referenceData = _idToReferenceData[id];
if (referenceData == null) { if (referenceData == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Id does not exist')]); throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Id does not exist')]);
} }
...@@ -3731,3 +3763,54 @@ class _WidgetFactory { ...@@ -3731,3 +3763,54 @@ class _WidgetFactory {
// recognize the annotation. // recognize the annotation.
// ignore: library_private_types_in_public_api // ignore: library_private_types_in_public_api
const _WidgetFactory widgetFactory = _WidgetFactory(); const _WidgetFactory widgetFactory = _WidgetFactory();
/// Does not hold keys from garbage collection.
@visibleForTesting
class WeakMap<K, V> {
Expando<Object> _objects = Expando<Object>();
/// Strings, numbers, booleans.
final Map<K, V> _primitives = <K, V>{};
bool _isPrimitive(Object? key) {
return key == null || key is String || key is num || key is bool;
}
/// Returns the value for the given [key] or null if [key] is not in the map
/// or garbage collected.
///
/// Does not support records to act as keys.
V? operator [](K key){
if (_isPrimitive(key)) {
return _primitives[key];
} else {
return _objects[key!] as V?;
}
}
/// Associates the [key] with the given [value].
void operator []=(K key, V value){
if (_isPrimitive(key)) {
_primitives[key] = value;
} else {
_objects[key!] = value;
}
}
/// Removes the value for the given [key] from the map.
V? remove(K key) {
if (_isPrimitive(key)) {
return _primitives.remove(key);
} else {
final V? result = _objects[key!] as V?;
_objects[key] = null;
return result;
}
}
/// Removes all pairs from the map.
void clear() {
_objects = Expando<Object>();
_primitives.clear();
}
}
...@@ -1592,13 +1592,13 @@ void main() { ...@@ -1592,13 +1592,13 @@ void main() {
builder.properties.any((DiagnosticsNode property) => property.name == 'dependencies' && property.value != null), builder.properties.any((DiagnosticsNode property) => property.name == 'dependencies' && property.value != null),
isTrue, isTrue,
); );
final DiagnosticsProperty<List<DiagnosticsNode>> dependenciesProperty = final DiagnosticsProperty<Set<InheritedElement>> dependenciesProperty =
builder.properties.firstWhere((DiagnosticsNode property) => property.name == 'dependencies') as DiagnosticsProperty<List<DiagnosticsNode>>; builder.properties.firstWhere((DiagnosticsNode property) => property.name == 'dependencies') as DiagnosticsProperty<Set<InheritedElement>>;
expect(dependenciesProperty, isNotNull); expect(dependenciesProperty, isNotNull);
final List<DiagnosticsNode> dependencies = dependenciesProperty.value!; final Set<InheritedElement> dependencies = dependenciesProperty.value!;
expect(dependencies.length, equals(3)); expect(dependencies.length, equals(3));
expect(dependencies.toString(), '[ButtonBarTheme, Directionality, FocusTraversalOrder]'); expect(dependenciesProperty.toDescription(), '[ButtonBarTheme, Directionality, FocusTraversalOrder]');
}); });
testWidgets('BuildOwner.globalKeyCount keeps track of in-use global keys', (WidgetTester tester) async { testWidgets('BuildOwner.globalKeyCount keeps track of in-use global keys', (WidgetTester tester) async {
......
...@@ -240,7 +240,41 @@ extension TextFromString on String { ...@@ -240,7 +240,41 @@ extension TextFromString on String {
} }
} }
final List<Object> _weakValueTests = <Object>[1, 1.0, 'hello', true, false, Object(), <int>[3, 4], DateTime(2023)];
void main() { void main() {
group('$InspectorReferenceData', (){
for (final Object item in _weakValueTests) {
test('can be created for any type but $Record, $item', () async {
final InspectorReferenceData weakValue = InspectorReferenceData(item, 'id');
expect(weakValue.value, item);
});
}
test('throws for $Record', () async {
expect(()=> InspectorReferenceData((1, 2), 'id'), throwsA(isA<ArgumentError>()));
});
});
group('$WeakMap', (){
for (final Object item in _weakValueTests) {
test('assigns and removes value, $item', () async {
final WeakMap<Object, Object> weakMap = WeakMap<Object, Object>();
weakMap[item] = 1;
expect(weakMap[item], 1);
expect(weakMap.remove(item), 1);
expect(weakMap[item], null);
});
}
for (final Object item in _weakValueTests) {
test('returns null for absent value, $item', () async {
final WeakMap<Object, Object> weakMap = WeakMap<Object, Object>();
expect(weakMap[item], null);
});
}
});
_TestWidgetInspectorService.runTests(); _TestWidgetInspectorService.runTests();
} }
...@@ -2184,7 +2218,8 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ...@@ -2184,7 +2218,8 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
final List<DiagnosticsNode> expectedProperties = element.toDiagnosticsNode().getProperties(); final List<DiagnosticsNode> expectedProperties = element.toDiagnosticsNode().getProperties();
final Iterable<Object?> propertyValues = expectedProperties.map((DiagnosticsNode e) => e.value.toString()); final Iterable<Object?> propertyValues = expectedProperties.map((DiagnosticsNode e) => e.value.toString());
for (final Map<String, Object?> propertyJson in propertiesJson.cast<Map<String, Object?>>()) { for (final Map<String, Object?> propertyJson in propertiesJson.cast<Map<String, Object?>>()) {
final String property = service.toObject(propertyJson['valueId']! as String)!.toString(); final String id = propertyJson['valueId']! as String;
final String property = service.toObject(id)!.toString();
expect(propertyValues, contains(property)); expect(propertyValues, contains(property));
} }
} }
...@@ -2227,7 +2262,8 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ...@@ -2227,7 +2262,8 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
final List<DiagnosticsNode> expectedProperties = element.toDiagnosticsNode().getProperties(); final List<DiagnosticsNode> expectedProperties = element.toDiagnosticsNode().getProperties();
final Iterable<Object?> propertyValues = expectedProperties.map((DiagnosticsNode e) => e.value.toString()); final Iterable<Object?> propertyValues = expectedProperties.map((DiagnosticsNode e) => e.value.toString());
for (final Map<String, Object?> propertyJson in propertiesJson.cast<Map<String, Object?>>()) { for (final Map<String, Object?> propertyJson in propertiesJson.cast<Map<String, Object?>>()) {
final String property = service.toObject(propertyJson['valueId']! as String)!.toString(); final String id = propertyJson['valueId']! as String;
final String property = service.toObject(id)!.toString();
expect(propertyValues, contains(property)); expect(propertyValues, contains(property));
} }
} }
...@@ -2283,7 +2319,6 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ...@@ -2283,7 +2319,6 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
testWidgets('ext.flutter.inspector.getRootWidgetSummaryTree', (WidgetTester tester) async { testWidgets('ext.flutter.inspector.getRootWidgetSummaryTree', (WidgetTester tester) async {
const String group = 'test-group'; const String group = 'test-group';
await tester.pumpWidget( await tester.pumpWidget(
const Directionality( const Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
...@@ -2296,6 +2331,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ...@@ -2296,6 +2331,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
), ),
), ),
); );
final Element elementA = find.text('a').evaluate().first; final Element elementA = find.text('a').evaluate().first;
service.disposeAllGroups(); service.disposeAllGroups();
...@@ -2311,6 +2347,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ...@@ -2311,6 +2347,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
WidgetInspectorServiceExtensions.getRootWidgetSummaryTree.name, WidgetInspectorServiceExtensions.getRootWidgetSummaryTree.name,
<String, String>{'objectGroup': group}, <String, String>{'objectGroup': group},
))! as Map<String, Object?>; ))! as Map<String, Object?>;
// We haven't yet properly specified which directories are summary tree // We haven't yet properly specified which directories are summary tree
// directories so we get an empty tree other than the root that is always // directories so we get an empty tree other than the root that is always
// included. // included.
......
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