Skip to content

Commit edf312d

Browse files
authored
Add frame number and widget location map service extension (flutter#148702)
This helps us add widget rebuild counts to the DevTools performance page: flutter/devtools#4564
1 parent d57ea48 commit edf312d

File tree

4 files changed

+123
-9
lines changed

4 files changed

+123
-9
lines changed

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,22 @@ enum WidgetInspectorServiceExtensions {
144144
/// extension is registered.
145145
trackRebuildDirtyWidgets,
146146

147+
/// Name of service extension that, when called, returns the mapping of
148+
/// widget locations to ids.
149+
///
150+
/// This service extension is only supported if
151+
/// [WidgetInspectorService._widgetCreationTracked] is true.
152+
///
153+
/// See also:
154+
///
155+
/// * [trackRebuildDirtyWidgets], which toggles dispatching events that use
156+
/// these ids to efficiently indicate the locations of widgets.
157+
/// * [WidgetInspectorService.initServiceExtensions], where the service
158+
/// extension is registered.
159+
widgetLocationIdMap,
160+
147161
/// Name of service extension that, when called, determines whether
162+
/// [WidgetInspectorService._trackRepaintWidgets], which determines whether
148163
/// a callback is invoked for every [RenderObject] painted each frame.
149164
///
150165
/// See also:

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

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,6 +1114,14 @@ mixin WidgetInspectorService {
11141114
registerExtension: registerExtension,
11151115
);
11161116

1117+
_registerSignalServiceExtension(
1118+
name: WidgetInspectorServiceExtensions.widgetLocationIdMap.name,
1119+
callback: () {
1120+
return _locationIdMapToJson();
1121+
},
1122+
registerExtension: registerExtension,
1123+
);
1124+
11171125
_registerBoolServiceExtension(
11181126
name: WidgetInspectorServiceExtensions.trackRepaintWidgets.name,
11191127
getter: () async => _trackRepaintWidgets,
@@ -2365,9 +2373,11 @@ mixin WidgetInspectorService {
23652373
bool? _widgetCreationTracked;
23662374

23672375
late Duration _frameStart;
2376+
late int _frameNumber;
23682377

23692378
void _onFrameStart(Duration timeStamp) {
23702379
_frameStart = timeStamp;
2380+
_frameNumber = PlatformDispatcher.instance.frameData.frameNumber;
23712381
SchedulerBinding.instance.addPostFrameCallback(_onFrameEnd, debugLabel: 'WidgetInspector.onFrameStart');
23722382
}
23732383

@@ -2381,7 +2391,13 @@ mixin WidgetInspectorService {
23812391
}
23822392

23832393
void _postStatsEvent(String eventName, _ElementLocationStatsTracker stats) {
2384-
postEvent(eventName, stats.exportToJson(_frameStart));
2394+
postEvent(
2395+
eventName,
2396+
stats.exportToJson(
2397+
_frameStart,
2398+
frameNumber: _frameNumber,
2399+
),
2400+
);
23852401
}
23862402

23872403
/// All events dispatched by a [WidgetInspectorService] use this method
@@ -2590,7 +2606,7 @@ class _ElementLocationStatsTracker {
25902606

25912607
/// Exports the current counts and then resets the stats to prepare to track
25922608
/// the next frame of data.
2593-
Map<String, dynamic> exportToJson(Duration startTime) {
2609+
Map<String, dynamic> exportToJson(Duration startTime, {required int frameNumber}) {
25942610
final List<int> events = List<int>.filled(active.length * 2, 0);
25952611
int j = 0;
25962612
for (final _LocationCount stat in active) {
@@ -2600,6 +2616,7 @@ class _ElementLocationStatsTracker {
26002616

26012617
final Map<String, dynamic> json = <String, dynamic>{
26022618
'startTime': startTime.inMicroseconds,
2619+
'frameNumber': frameNumber,
26032620
'events': events,
26042621
};
26052622

@@ -3246,12 +3263,21 @@ class _InspectorOverlayLayer extends Layer {
32463263
final Rect targetRect = MatrixUtils.transformRect(
32473264
state.selected.transform, state.selected.rect,
32483265
);
3249-
final Offset target = Offset(targetRect.left, targetRect.center.dy);
3250-
const double offsetFromWidget = 9.0;
3251-
final double verticalOffset = (targetRect.height) / 2 + offsetFromWidget;
3252-
3253-
_paintDescription(canvas, state.tooltip, state.textDirection, target, verticalOffset, size, targetRect);
3254-
3266+
if (!targetRect.hasNaN) {
3267+
final Offset target = Offset(targetRect.left, targetRect.center.dy);
3268+
const double offsetFromWidget = 9.0;
3269+
final double verticalOffset = (targetRect.height) / 2 + offsetFromWidget;
3270+
3271+
_paintDescription(
3272+
canvas,
3273+
state.tooltip,
3274+
state.textDirection,
3275+
target,
3276+
verticalOffset,
3277+
size,
3278+
targetRect,
3279+
);
3280+
}
32553281
// TODO(jacobr): provide an option to perform a debug paint of just the
32563282
// selected widget.
32573283
return recorder.endRecording();
@@ -3630,6 +3656,34 @@ int _toLocationId(_Location location) {
36303656
return id;
36313657
}
36323658

3659+
Map<String, dynamic> _locationIdMapToJson() {
3660+
const String idsKey = 'ids';
3661+
const String linesKey = 'lines';
3662+
const String columnsKey = 'columns';
3663+
const String namesKey = 'names';
3664+
3665+
final Map<String, Map<String, List<Object?>>> fileLocationsMap =
3666+
<String, Map<String, List<Object?>>>{};
3667+
for (final MapEntry<_Location, int> entry in _locationToId.entries) {
3668+
final _Location location = entry.key;
3669+
final Map<String, List<Object?>> locations = fileLocationsMap.putIfAbsent(
3670+
location.file,
3671+
() => <String, List<Object?>>{
3672+
idsKey: <int>[],
3673+
linesKey: <int>[],
3674+
columnsKey: <int>[],
3675+
namesKey: <String?>[],
3676+
},
3677+
);
3678+
3679+
locations[idsKey]!.add(entry.value);
3680+
locations[linesKey]!.add(location.line);
3681+
locations[columnsKey]!.add(location.column);
3682+
locations[namesKey]!.add(location.name);
3683+
}
3684+
return fileLocationsMap;
3685+
}
3686+
36333687
/// A delegate that configures how a hierarchy of [DiagnosticsNode]s are
36343688
/// serialized by the Flutter Inspector.
36353689
@visibleForTesting

packages/flutter/test/foundation/service_extensions_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ void main() {
170170
if (WidgetInspectorService.instance.isWidgetCreationTracked()) {
171171
// Some inspector extensions are only exposed if widget creation locations
172172
// are tracked.
173-
widgetInspectorExtensionCount += 2;
173+
widgetInspectorExtensionCount += 3;
174174
}
175175
expect(binding.extensions.keys.where((String name) => name.startsWith('inspector.')), hasLength(widgetInspectorExtensionCount));
176176

packages/flutter/test/widgets/widget_inspector_test.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3765,6 +3765,49 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
37653765
skip: !WidgetInspectorService.instance.isWidgetCreationTracked(), // [intended] Test requires --track-widget-creation flag.
37663766
);
37673767

3768+
testWidgets('ext.flutter.inspector.widgetLocationIdMap',
3769+
(WidgetTester tester) async {
3770+
service.rebuildCount = 0;
3771+
3772+
await tester.pumpWidget(const ClockDemo());
3773+
3774+
final Element clockDemoElement = find.byType(ClockDemo).evaluate().first;
3775+
3776+
service.setSelection(clockDemoElement, 'my-group');
3777+
final Map<String, Object?> jsonObject = (await service.testExtension(
3778+
WidgetInspectorServiceExtensions.getSelectedWidget.name,
3779+
<String, String>{'objectGroup': 'my-group'},
3780+
))! as Map<String, Object?>;
3781+
final Map<String, Object?> creationLocation =
3782+
jsonObject['creationLocation']! as Map<String, Object?>;
3783+
final String file = creationLocation['file']! as String;
3784+
expect(file, endsWith('widget_inspector_test.dart'));
3785+
3786+
final Map<String, Object?> locationMapJson = (await service.testExtension(
3787+
WidgetInspectorServiceExtensions.widgetLocationIdMap.name,
3788+
<String, String>{},
3789+
))! as Map<String, Object?>;
3790+
3791+
final Map<String, Object?> widgetTestLocations =
3792+
locationMapJson[file]! as Map<String, Object?>;
3793+
expect(widgetTestLocations, isNotNull);
3794+
3795+
final List<dynamic> ids = widgetTestLocations['ids']! as List<dynamic>;
3796+
expect(ids.length, greaterThan(0));
3797+
final List<dynamic> lines =
3798+
widgetTestLocations['lines']! as List<dynamic>;
3799+
expect(lines.length, equals(ids.length));
3800+
final List<dynamic> columns =
3801+
widgetTestLocations['columns']! as List<dynamic>;
3802+
expect(columns.length, equals(ids.length));
3803+
final List<dynamic> names =
3804+
widgetTestLocations['names']! as List<dynamic>;
3805+
expect(names.length, equals(ids.length));
3806+
expect(names, contains('ClockDemo'));
3807+
expect(names, contains('Directionality'));
3808+
expect(names, contains('ClockText'));
3809+
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag.
3810+
37683811
testWidgets('ext.flutter.inspector.trackRebuildDirtyWidgets', (WidgetTester tester) async {
37693812
service.rebuildCount = 0;
37703813

@@ -3951,6 +3994,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
39513994
expect(rebuildEvents.length, equals(1));
39523995
event = removeLastEvent(rebuildEvents);
39533996
expect(event['startTime'], isA<int>());
3997+
expect(event['frameNumber'], isA<int>());
39543998
data = event['events']! as List<int>;
39553999
newLocations = event['newLocations']! as Map<String, List<int>>;
39564000
fileLocationsMap = event['locations']! as Map<String, Map<String, List<Object?>>>;
@@ -4080,6 +4124,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
40804124
expect(repaintEvents.length, equals(1));
40814125
event = removeLastEvent(repaintEvents);
40824126
expect(event['startTime'], isA<int>());
4127+
expect(event['frameNumber'], isA<int>());
40834128
data = event['events']! as List<int>;
40844129
// No new locations were rebuilt.
40854130
expect(event, isNot(contains('newLocations')));

0 commit comments

Comments
 (0)