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 {