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
This commit is contained in:
Helin Shiah 2024-05-22 14:29:27 -07:00 committed by GitHub
parent d57ea48ca1
commit edf312d506
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 122 additions and 8 deletions

View File

@ -144,7 +144,22 @@ enum WidgetInspectorServiceExtensions {
/// 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:

View File

@ -1114,6 +1114,14 @@ mixin WidgetInspectorService {
registerExtension: registerExtension,
);
_registerSignalServiceExtension(
name: WidgetInspectorServiceExtensions.widgetLocationIdMap.name,
callback: () {
return _locationIdMapToJson();
},
registerExtension: registerExtension,
);
_registerBoolServiceExtension(
name: WidgetInspectorServiceExtensions.trackRepaintWidgets.name,
getter: () async => _trackRepaintWidgets,
@ -2365,9 +2373,11 @@ mixin WidgetInspectorService {
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 @@ mixin WidgetInspectorService {
}
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 @@ class _ElementLocationStatsTracker {
/// 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 @@ class _ElementLocationStatsTracker {
final Map<String, dynamic> json = <String, dynamic>{
'startTime': startTime.inMicroseconds,
'frameNumber': frameNumber,
'events': events,
};
@ -3246,12 +3263,21 @@ class _InspectorOverlayLayer extends Layer {
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;
_paintDescription(canvas, state.tooltip, state.textDirection, target, verticalOffset, size, targetRect);
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,
);
}
// TODO(jacobr): provide an option to perform a debug paint of just the
// selected widget.
return recorder.endRecording();
@ -3630,6 +3656,34 @@ int _toLocationId(_Location location) {
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

View File

@ -170,7 +170,7 @@ void main() {
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));

View File

@ -3765,6 +3765,49 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
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 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
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 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
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')));