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')));