diff --git a/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_controller.dart b/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_controller.dart index 309b5c7b4a4..63ee814e46b 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_controller.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_controller.dart @@ -22,7 +22,7 @@ class FlutterFramesController extends PerformanceFeatureController { /// The currently selected timeline frame. ValueListenable get selectedFrame => _selectedFrameNotifier; - final _selectedFrameNotifier = ValueNotifier(null); + final _selectedFrameNotifier = _SelectedFrameNotifier(null); /// The flutter frames in the chart. ValueListenable> get flutterFrames => _flutterFrames; @@ -123,6 +123,12 @@ class FlutterFramesController extends PerformanceFeatureController { _unassignedFlutterFrames.remove(frameNumber); _updateFirstWellFormedFrameMicros(frame); } + if (_selectedFrameNotifier.value == frame) { + // [frame] was selected before its timeline events were available. Notify + // listeners again so that views built from [selectedFrame], like the + // Frame Analysis tab, rebuild with the data that is now available. + _selectedFrameNotifier.notifyFrameDataChanged(); + } } bool hasUnassignedFlutterFrame(int frameNumber) { @@ -223,3 +229,16 @@ class FlutterFramesController extends PerformanceFeatureController { super.dispose(); } } + +/// Notifier for the selected [FlutterFrame]. +/// +/// Timeline events are assigned to a [FlutterFrame] in place, which can happen +/// after the frame has been selected. [ValueNotifier] only notifies when +/// [value] changes, and [notifyListeners] is protected, so this subclass +/// exposes [notifyFrameDataChanged] for that case. +class _SelectedFrameNotifier extends ValueNotifier { + _SelectedFrameNotifier(super.value); + + /// Notifies listeners that the selected frame's data changed in place. + void notifyFrameDataChanged() => notifyListeners(); +} diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 7d47c6b982a..92366f83803 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -24,7 +24,10 @@ TODO: Remove this section if there are not any updates. ## Performance updates -TODO: Remove this section if there are not any updates. +* Fixed the Frame Analysis tab showing "No timeline event analysis data + available for this frame" when a frame is selected before its timeline events + have been fetched from the VM. + [#10050](https://github.com/flutter/devtools/pull/10050) ## CPU profiler updates diff --git a/packages/devtools_app/test/screens/performance/flutter_frames/flutter_frames_controller_test.dart b/packages/devtools_app/test/screens/performance/flutter_frames/flutter_frames_controller_test.dart index 1448a26f2be..fd5a718c7a0 100644 --- a/packages/devtools_app/test/screens/performance/flutter_frames/flutter_frames_controller_test.dart +++ b/packages/devtools_app/test/screens/performance/flutter_frames/flutter_frames_controller_test.dart @@ -87,6 +87,43 @@ void main() { expect(framesController.selectedFrame.value, equals(FlutterFrame4.frame)); }); + test('notifies listeners when events are assigned to selected frame', () { + when( + // ignore: discarded_futures, by design + mockTimelineEventsController.handleSelectedFrame(any), + ).thenAnswer((_) => Future.value()); + + final frame = FlutterFrame.fromJson({ + 'number': 3, + 'startTime': 40000, + 'elapsed': 20000, + 'build': 16000, + 'raster': 16000, + 'vsyncOverhead': 1000, + }); + framesController.addFrame(frame); + framesController.handleSelectedFrame(frame); + expect(frame.frameAnalysis, isNull); + + var notifyCount = 0; + framesController.selectedFrame.addListener(() => notifyCount++); + + // Assigning an event to the selected frame should notify listeners, even + // though the selected frame instance has not changed. + framesController.assignEventToFrame(frame.id, FlutterFrame2.uiEvent); + expect(notifyCount, equals(1)); + expect(framesController.selectedFrame.value, equals(frame)); + expect(frame.frameAnalysis, isNotNull); + + // Assigning an event to a frame that is not selected should not notify. + framesController.addFrame(testFrame2); + framesController.assignEventToFrame( + testFrame2.id, + FlutterFrame2.rasterEvent, + ); + expect(notifyCount, equals(1)); + }); + test('can setOfflineData', () async { // Ensure we are starting in an empty state. expect(framesController.flutterFrames.value, isEmpty);