Unverified Commit e3f15a5b authored by flutteractionsbot's avatar flutteractionsbot Committed by GitHub

[CP-stable] Add frame number and widget location map service extension (#149345)

This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/wiki/Flutter-Cherrypick-Process#automatically-creates-a-cherry-pick-request)
Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request.

### Issue Link:
What is the link to the issue this cherry-pick is addressing?

DevTools release 2.36.0: https://dart-review.googlesource.com/c/sdk/+/368762

### Changelog Description:

This change adds a service extension that DevTools uses to support a "Track widget build counts" feature. This feature is first included in DevTools 2.36.0.

### Impact Description:

This service extension is required for DevTools 2.36.0, which needs to be [cherry-picked](https://dart-review.googlesource.com/c/sdk/+/368762) into the Dart SDK. This beta release of DevTools missed the code cutoff because we were targeting the date listed on go/dash-team-releases (5/29). Since the beta release went out early, we missed the cut off and need to cherry-pick.

Since this DevTools release depended on the changes from https://github.com/flutter/flutter/pull/148702, we also need to cherry pick those changes into the flutter beta channel.

### Workaround:
No. This is required for DevTools 2.36.0 to work properly: https://dart-review.googlesource.com/c/sdk/+/368762.

### Risk:
What is the risk level of this cherry-pick?

### Test Coverage:
Are you confident that your fix is well-tested by automated tests?

### Validation Steps:
What are the steps to validate that this fix works?

With this change, a flutter app connecting to DevTools 2.36.0 (already on the Dart SDK main branch and trying to CP into the Dart SDK beta branch) will be able to use the "Track widget build counts" feature in DevTools.
parent 95ad5b54
......@@ -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:
......
......@@ -1120,6 +1120,14 @@ mixin WidgetInspectorService {
registerExtension: registerExtension,
);
_registerSignalServiceExtension(
name: WidgetInspectorServiceExtensions.widgetLocationIdMap.name,
callback: () {
return _locationIdMapToJson();
},
registerExtension: registerExtension,
);
_registerBoolServiceExtension(
name: WidgetInspectorServiceExtensions.trackRepaintWidgets.name,
getter: () async => _trackRepaintWidgets,
......@@ -2375,9 +2383,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');
}
......@@ -2391,7 +2401,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
......@@ -2600,7 +2616,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) {
......@@ -2610,6 +2626,7 @@ class _ElementLocationStatsTracker {
final Map<String, dynamic> json = <String, dynamic>{
'startTime': startTime.inMicroseconds,
'frameNumber': frameNumber,
'events': events,
};
......@@ -3256,12 +3273,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();
......@@ -3651,6 +3677,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
......
......@@ -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));
......
......@@ -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')));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment