Fix Flutter frames chart freeze after switching screens (#9960)
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_chart.dart b/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_chart.dart
index 82bee01..27e5cce 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_chart.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_chart.dart
@@ -249,6 +249,14 @@
_framesScrollController = ScrollController(
initialScrollOffset: initialScrollOffset,
);
+
+ // Snap after layout so [atScrollBottom] matches for live follow.
+ if (_selectedFrameIndex == null) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (!mounted || !_framesScrollController.hasClients) return;
+ unawaited(_framesScrollController.autoScrollToBottom(jump: true));
+ });
+ }
}
@override
@@ -261,13 +269,20 @@
}
double _calculateInitialHorizontalScrollOffset() {
- final selectedIndex = _selectedFrameIndex;
- if (selectedIndex == null) return 0.0;
-
- final chartWidthWithoutAxisLabels =
- widget.constraints.maxWidth - _yAxisUnitsSpace;
+ final chartWidthWithoutAxisLabels = math.max(
+ 0.0,
+ widget.constraints.maxWidth - _yAxisUnitsSpace,
+ );
final totalFramesInView =
chartWidthWithoutAxisLabels ~/ _defaultFrameWidthWithPadding;
+
+ final selectedIndex = _selectedFrameIndex;
+ if (selectedIndex == null) {
+ // Dock to the live edge when remounting with no selection (#9525).
+ final framesOutOfView = widget.frames.length - totalFramesInView;
+ return math.max(0.0, framesOutOfView * _defaultFrameWidthWithPadding);
+ }
+
final fullFrameRangeInView = Range(0, totalFramesInView);
if (fullFrameRangeInView.contains(selectedIndex)) return 0.0;
diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
index b8955e4..a54592a 100644
--- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
+++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
@@ -28,7 +28,9 @@
## Performance updates
-TODO: Remove this section if there are not any updates.
+* Fixed a bug where the Flutter frames chart could appear frozen after
+ switching away from the Performance screen and coming back.
+ [#9960](https://github.com/flutter/devtools/pull/9960)
## CPU profiler updates
diff --git a/packages/devtools_app/test/screens/performance/flutter_frames/flutter_frames_chart_test.dart b/packages/devtools_app/test/screens/performance/flutter_frames/flutter_frames_chart_test.dart
index 0945d83..10679f0 100644
--- a/packages/devtools_app/test/screens/performance/flutter_frames/flutter_frames_chart_test.dart
+++ b/packages/devtools_app/test/screens/performance/flutter_frames/flutter_frames_chart_test.dart
@@ -155,7 +155,29 @@
expect(scrollController.offset, equals(expectedOffset));
}
- testWidgets('is zero for no selected frame', (WidgetTester tester) async {
+ void verifyScrollAtLiveEdge(WidgetTester tester) {
+ final scrollbar = tester.widget<Scrollbar>(find.byType(Scrollbar));
+ final scrollController = scrollbar.controller!;
+ expect(
+ scrollController.offset,
+ equals(scrollController.position.maxScrollExtent),
+ );
+ }
+
+ FlutterFrame createFrame(int number) {
+ return FlutterFrame.fromJson({
+ 'number': number,
+ 'startTime': 10000 + number * 50000,
+ 'elapsed': 20000,
+ 'build': 10000,
+ 'raster': 12000,
+ 'vsyncOverhead': 10,
+ });
+ }
+
+ testWidgets('is at live edge for no selected frame', (
+ WidgetTester tester,
+ ) async {
expect(framesController.selectedFrame.value, isNull);
await pumpChart(tester);
@@ -165,7 +187,39 @@
findsNWidgets(totalFramesInView),
);
- verifyScrollOffset(tester, 0.0);
+ verifyScrollAtLiveEdge(tester);
+ });
+
+ testWidgets('stays at live edge when frames are added', (
+ WidgetTester tester,
+ ) async {
+ expect(framesController.selectedFrame.value, isNull);
+
+ await pumpChart(tester);
+ verifyScrollAtLiveEdge(tester);
+
+ framesController.addFrame(createFrame(totalNumFrames));
+ await tester.pumpAndSettle();
+ verifyScrollAtLiveEdge(tester);
+ });
+
+ testWidgets('docks to live edge after remount with no selection', (
+ WidgetTester tester,
+ ) async {
+ expect(framesController.selectedFrame.value, isNull);
+
+ await pumpChart(tester);
+ verifyScrollAtLiveEdge(tester);
+
+ // Leave Performance (dispose chart), then remount after new frames.
+ await tester.pumpWidget(wrap(const SizedBox.shrink()));
+ await tester.pumpAndSettle();
+
+ framesController.addFrame(createFrame(totalNumFrames));
+ framesController.addFrame(createFrame(totalNumFrames + 1));
+
+ await pumpChart(tester);
+ verifyScrollAtLiveEdge(tester);
});
testWidgets('is offset for selected frame', (WidgetTester tester) async {