Skip to content

Commit be60723

Browse files
helin24GitHub Actions Bot
authored andcommitted
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 a14f74f commit be60723

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
@@ -1120,6 +1120,14 @@ mixin WidgetInspectorService {
11201120
registerExtension: registerExtension,
11211121
);
11221122

1123+
_registerSignalServiceExtension(
1124+
name: WidgetInspectorServiceExtensions.widgetLocationIdMap.name,
1125+
callback: () {
1126+
return _locationIdMapToJson();
1127+
},
1128+
registerExtension: registerExtension,
1129+
);
1130+
11231131
_registerBoolServiceExtension(
11241132
name: WidgetInspectorServiceExtensions.trackRepaintWidgets.name,
11251133
getter: () async => _trackRepaintWidgets,
@@ -2375,9 +2383,11 @@ mixin WidgetInspectorService {
23752383
bool? _widgetCreationTracked;
23762384

23772385
late Duration _frameStart;
2386+
late int _frameNumber;
23782387

23792388
void _onFrameStart(Duration timeStamp) {
23802389
_frameStart = timeStamp;
2390+
_frameNumber = PlatformDispatcher.instance.frameData.frameNumber;
23812391
SchedulerBinding.instance.addPostFrameCallback(_onFrameEnd, debugLabel: 'WidgetInspector.onFrameStart');
23822392
}
23832393

@@ -2391,7 +2401,13 @@ mixin WidgetInspectorService {
23912401
}
23922402

23932403
void _postStatsEvent(String eventName, _ElementLocationStatsTracker stats) {
2394-
postEvent(eventName, stats.exportToJson(_frameStart));
2404+
postEvent(
2405+
eventName,
2406+
stats.exportToJson(
2407+
_frameStart,
2408+
frameNumber: _frameNumber,
2409+
),
2410+
);
23952411
}
23962412

23972413
/// All events dispatched by a [WidgetInspectorService] use this method
@@ -2600,7 +2616,7 @@ class _ElementLocationStatsTracker {
26002616

26012617
/// Exports the current counts and then resets the stats to prepare to track
26022618
/// the next frame of data.
2603-
Map<String, dynamic> exportToJson(Duration startTime) {
2619+
Map<String, dynamic> exportToJson(Duration startTime, {required int frameNumber}) {
26042620
final List<int> events = List<int>.filled(active.length * 2, 0);
26052621
int j = 0;
26062622
for (final _LocationCount stat in active) {
@@ -2610,6 +2626,7 @@ class _ElementLocationStatsTracker {
26102626

26112627
final Map<String, dynamic> json = <String, dynamic>{
26122628
'startTime': startTime.inMicroseconds,
2629+
'frameNumber': frameNumber,
26132630
'events': events,
26142631
};
26152632

@@ -3256,12 +3273,21 @@ class _InspectorOverlayLayer extends Layer {
32563273
final Rect targetRect = MatrixUtils.transformRect(
32573274
state.selected.transform, state.selected.rect,
32583275
);
3259-
final Offset target = Offset(targetRect.left, targetRect.center.dy);
3260-
const double offsetFromWidget = 9.0;
3261-
final double verticalOffset = (targetRect.height) / 2 + offsetFromWidget;
3262-
3263-
_paintDescription(canvas, state.tooltip, state.textDirection, target, verticalOffset, size, targetRect);
3264-
3276+
if (!targetRect.hasNaN) {
3277+
final Offset target = Offset(targetRect.left, targetRect.center.dy);
3278+
const double offsetFromWidget = 9.0;
3279+
final double verticalOffset = (targetRect.height) / 2 + offsetFromWidget;
3280+
3281+
_paintDescription(
3282+
canvas,
3283+
state.tooltip,
3284+
state.textDirection,
3285+
target,
3286+
verticalOffset,
3287+
size,
3288+
targetRect,
3289+
);
3290+
}
32653291
// TODO(jacobr): provide an option to perform a debug paint of just the
32663292
// selected widget.
32673293
return recorder.endRecording();
@@ -3651,6 +3677,34 @@ int _toLocationId(_Location location) {
36513677
return id;
36523678
}
36533679

3680+
Map<String, dynamic> _locationIdMapToJson() {
3681+
const String idsKey = 'ids';
3682+
const String linesKey = 'lines';
3683+
const String columnsKey = 'columns';
3684+
const String namesKey = 'names';
3685+
3686+
final Map<String, Map<String, List<Object?>>> fileLocationsMap =
3687+
<String, Map<String, List<Object?>>>{};
3688+
for (final MapEntry<_Location, int> entry in _locationToId.entries) {
3689+
final _Location location = entry.key;
3690+
final Map<String, List<Object?>> locations = fileLocationsMap.putIfAbsent(
3691+
location.file,
3692+
() => <String, List<Object?>>{
3693+
idsKey: <int>[],
3694+
linesKey: <int>[],
3695+
columnsKey: <int>[],
3696+
namesKey: <String?>[],
3697+
},
3698+
);
3699+
3700+
locations[idsKey]!.add(entry.value);
3701+
locations[linesKey]!.add(location.line);
3702+
locations[columnsKey]!.add(location.column);
3703+
locations[namesKey]!.add(location.name);
3704+
}
3705+
return fileLocationsMap;
3706+
}
3707+
36543708
/// A delegate that configures how a hierarchy of [DiagnosticsNode]s are
36553709
/// serialized by the Flutter Inspector.
36563710
@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)