From 93c155e62addc1267c17b8a49c812e709aa74116 Mon Sep 17 00:00:00 2001 From: khanak0509 Date: Thu, 13 Aug 2026 02:34:25 +0530 Subject: [PATCH 1/3] Fix Flutter frames chart freeze after switching screens --- .../flutter_frames/flutter_frames_chart.dart | 19 +++++- .../flutter_frames_chart_test.dart | 58 ++++++++++++++++++- 2 files changed, 72 insertions(+), 5 deletions(-) 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 82bee01d071..c2d02af5661 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 @@ class _FramesChartState extends State with AutoDisposeMixin { _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,18 @@ class _FramesChartState extends State with AutoDisposeMixin { } double _calculateInitialHorizontalScrollOffset() { - final selectedIndex = _selectedFrameIndex; - if (selectedIndex == null) return 0.0; - final chartWidthWithoutAxisLabels = 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/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 0945d8321f1..10679f09c74 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 @@ void main() { expect(scrollController.offset, equals(expectedOffset)); } - testWidgets('is zero for no selected frame', (WidgetTester tester) async { + void verifyScrollAtLiveEdge(WidgetTester tester) { + final scrollbar = tester.widget(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 @@ void main() { 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 { From 077537b8ee271cba0c004ce4bb705d957eb55a17 Mon Sep 17 00:00:00 2001 From: khanak0509 Date: Thu, 13 Aug 2026 02:46:36 +0530 Subject: [PATCH 2/3] Add release note --- packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 9bbf8ecebc3..2f49eaea7e4 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -23,7 +23,9 @@ TODO: Remove this section if there are not any updates. ## 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 From d0e0e6857b1d80a3cf921777f071c1d2a0650755 Mon Sep 17 00:00:00 2001 From: Khanak Khandelwal Date: Thu, 13 Aug 2026 02:48:51 +0530 Subject: [PATCH 3/3] Update packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_chart.dart Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../performance/panes/flutter_frames/flutter_frames_chart.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c2d02af5661..006058fd074 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 @@ -270,7 +270,7 @@ class _FramesChartState extends State with AutoDisposeMixin { double _calculateInitialHorizontalScrollOffset() { final chartWidthWithoutAxisLabels = - widget.constraints.maxWidth - _yAxisUnitsSpace; + math.max(0.0, widget.constraints.maxWidth - _yAxisUnitsSpace); final totalFramesInView = chartWidthWithoutAxisLabels ~/ _defaultFrameWidthWithPadding;