Add frame number and widget location map service extension (#148702)
This helps us add widget rebuild counts to the DevTools performance page: https://github.com/flutter/devtools/issues/4564
diff --git a/packages/flutter/lib/src/widgets/service_extensions.dart b/packages/flutter/lib/src/widgets/service_extensions.dart
index caeb190..54971b5 100644
--- a/packages/flutter/lib/src/widgets/service_extensions.dart
+++ b/packages/flutter/lib/src/widgets/service_extensions.dart
@@ -144,7 +144,22 @@
/// extension is registered.
trackRebuildDirtyWidgets,
+ /// Name of service extension that, when called, returns the mapping of
+ /// widget locations to ids.
+ ///
+ /// This service extension is only supported if
+ /// [WidgetInspectorService._widgetCreationTracked] is true.
+ ///
+ /// See also:
+ ///
+ /// * [trackRebuildDirtyWidgets], which toggles dispatching events that use
+ /// these ids to efficiently indicate the locations of widgets.
+ /// * [WidgetInspectorService.initServiceExtensions], where the service
+ /// extension is registered.
+ widgetLocationIdMap,
+
/// Name of service extension that, when called, determines whether
+ /// [WidgetInspectorService._trackRepaintWidgets], which determines whether
/// a callback is invoked for every [RenderObject] painted each frame.
///
/// See also:
diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart
index 356b71c..5b28420 100644
--- a/packages/flutter/lib/src/widgets/widget_inspector.dart
+++ b/packages/flutter/lib/src/widgets/widget_inspector.dart
@@ -1114,6 +1114,14 @@
registerExtension: registerExtension,
);
+ _registerSignalServiceExtension(
+ name: WidgetInspectorServiceExtensions.widgetLocationIdMap.name,
+ callback: () {
+ return _locationIdMapToJson();
+ },
+ registerExtension: registerExtension,
+ );
+
_registerBoolServiceExtension(
name: WidgetInspectorServiceExtensions.trackRepaintWidgets.name,
getter: () async => _trackRepaintWidgets,
@@ -2365,9 +2373,11 @@
bool? _widgetCreationTracked;
late Duration _frameStart;
+ late int _frameNumber;
void _onFrameStart(Duration timeStamp) {
_frameStart = timeStamp;
+ _frameNumber = PlatformDispatcher.instance.frameData.frameNumber;
SchedulerBinding.instance.addPostFrameCallback(_onFrameEnd, debugLabel: 'WidgetInspector.onFrameStart');
}
@@ -2381,7 +2391,13 @@
}
void _postStatsEvent(String eventName, _ElementLocationStatsTracker stats) {
- postEvent(eventName, stats.exportToJson(_frameStart));
+ postEvent(
+ eventName,
+ stats.exportToJson(
+ _frameStart,
+ frameNumber: _frameNumber,
+ ),
+ );
}
/// All events dispatched by a [WidgetInspectorService] use this method
@@ -2590,7 +2606,7 @@
/// Exports the current counts and then resets the stats to prepare to track
/// the next frame of data.
- Map<String, dynamic> exportToJson(Duration startTime) {
+ Map<String, dynamic> exportToJson(Duration startTime, {required int frameNumber}) {
final List<int> events = List<int>.filled(active.length * 2, 0);
int j = 0;
for (final _LocationCount stat in active) {
@@ -2600,6 +2616,7 @@
final Map<String, dynamic> json = <String, dynamic>{
'startTime': startTime.inMicroseconds,
+ 'frameNumber': frameNumber,
'events': events,
};
@@ -3246,12 +3263,21 @@
final Rect targetRect = MatrixUtils.transformRect(
state.selected.transform, state.selected.rect,
);
- final Offset target = Offset(targetRect.left, targetRect.center.dy);
- const double offsetFromWidget = 9.0;
- final double verticalOffset = (targetRect.height) / 2 + offsetFromWidget;
+ if (!targetRect.hasNaN) {
+ final Offset target = Offset(targetRect.left, targetRect.center.dy);
+ const double offsetFromWidget = 9.0;
+ final double verticalOffset = (targetRect.height) / 2 + offsetFromWidget;
- _paintDescription(canvas, state.tooltip, state.textDirection, target, verticalOffset, size, targetRect);
-
+ _paintDescription(
+ canvas,
+ state.tooltip,
+ state.textDirection,
+ target,
+ verticalOffset,
+ size,
+ targetRect,
+ );
+ }
// TODO(jacobr): provide an option to perform a debug paint of just the
// selected widget.
return recorder.endRecording();
@@ -3630,6 +3656,34 @@
return id;
}
+Map<String, dynamic> _locationIdMapToJson() {
+ const String idsKey = 'ids';
+ const String linesKey = 'lines';
+ const String columnsKey = 'columns';
+ const String namesKey = 'names';
+
+ final Map<String, Map<String, List<Object?>>> fileLocationsMap =
+ <String, Map<String, List<Object?>>>{};
+ for (final MapEntry<_Location, int> entry in _locationToId.entries) {
+ final _Location location = entry.key;
+ final Map<String, List<Object?>> locations = fileLocationsMap.putIfAbsent(
+ location.file,
+ () => <String, List<Object?>>{
+ idsKey: <int>[],
+ linesKey: <int>[],
+ columnsKey: <int>[],
+ namesKey: <String?>[],
+ },
+ );
+
+ locations[idsKey]!.add(entry.value);
+ locations[linesKey]!.add(location.line);
+ locations[columnsKey]!.add(location.column);
+ locations[namesKey]!.add(location.name);
+ }
+ return fileLocationsMap;
+}
+
/// A delegate that configures how a hierarchy of [DiagnosticsNode]s are
/// serialized by the Flutter Inspector.
@visibleForTesting
diff --git a/packages/flutter/test/foundation/service_extensions_test.dart b/packages/flutter/test/foundation/service_extensions_test.dart
index b146f55..56ff355 100644
--- a/packages/flutter/test/foundation/service_extensions_test.dart
+++ b/packages/flutter/test/foundation/service_extensions_test.dart
@@ -170,7 +170,7 @@
if (WidgetInspectorService.instance.isWidgetCreationTracked()) {
// Some inspector extensions are only exposed if widget creation locations
// are tracked.
- widgetInspectorExtensionCount += 2;
+ widgetInspectorExtensionCount += 3;
}
expect(binding.extensions.keys.where((String name) => name.startsWith('inspector.')), hasLength(widgetInspectorExtensionCount));
diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart
index b40f8ed..32808e3 100644
--- a/packages/flutter/test/widgets/widget_inspector_test.dart
+++ b/packages/flutter/test/widgets/widget_inspector_test.dart
@@ -3765,6 +3765,49 @@
skip: !WidgetInspectorService.instance.isWidgetCreationTracked(), // [intended] Test requires --track-widget-creation flag.
);
+ testWidgets('ext.flutter.inspector.widgetLocationIdMap',
+ (WidgetTester tester) async {
+ service.rebuildCount = 0;
+
+ await tester.pumpWidget(const ClockDemo());
+
+ final Element clockDemoElement = find.byType(ClockDemo).evaluate().first;
+
+ service.setSelection(clockDemoElement, 'my-group');
+ final Map<String, Object?> jsonObject = (await service.testExtension(
+ WidgetInspectorServiceExtensions.getSelectedWidget.name,
+ <String, String>{'objectGroup': 'my-group'},
+ ))! as Map<String, Object?>;
+ final Map<String, Object?> creationLocation =
+ jsonObject['creationLocation']! as Map<String, Object?>;
+ final String file = creationLocation['file']! as String;
+ expect(file, endsWith('widget_inspector_test.dart'));
+
+ final Map<String, Object?> locationMapJson = (await service.testExtension(
+ WidgetInspectorServiceExtensions.widgetLocationIdMap.name,
+ <String, String>{},
+ ))! as Map<String, Object?>;
+
+ final Map<String, Object?> widgetTestLocations =
+ locationMapJson[file]! as Map<String, Object?>;
+ expect(widgetTestLocations, isNotNull);
+
+ final List<dynamic> ids = widgetTestLocations['ids']! as List<dynamic>;
+ expect(ids.length, greaterThan(0));
+ final List<dynamic> lines =
+ widgetTestLocations['lines']! as List<dynamic>;
+ expect(lines.length, equals(ids.length));
+ final List<dynamic> columns =
+ widgetTestLocations['columns']! as List<dynamic>;
+ expect(columns.length, equals(ids.length));
+ final List<dynamic> names =
+ widgetTestLocations['names']! as List<dynamic>;
+ expect(names.length, equals(ids.length));
+ expect(names, contains('ClockDemo'));
+ expect(names, contains('Directionality'));
+ expect(names, contains('ClockText'));
+ }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag.
+
testWidgets('ext.flutter.inspector.trackRebuildDirtyWidgets', (WidgetTester tester) async {
service.rebuildCount = 0;
@@ -3951,6 +3994,7 @@
expect(rebuildEvents.length, equals(1));
event = removeLastEvent(rebuildEvents);
expect(event['startTime'], isA<int>());
+ expect(event['frameNumber'], isA<int>());
data = event['events']! as List<int>;
newLocations = event['newLocations']! as Map<String, List<int>>;
fileLocationsMap = event['locations']! as Map<String, Map<String, List<Object?>>>;
@@ -4080,6 +4124,7 @@
expect(repaintEvents.length, equals(1));
event = removeLastEvent(repaintEvents);
expect(event['startTime'], isA<int>());
+ expect(event['frameNumber'], isA<int>());
data = event['events']! as List<int>;
// No new locations were rebuilt.
expect(event, isNot(contains('newLocations')));