Skip to content

Commit fd64bd7

Browse files
authored
Fix Flutter frames chart freeze after switching screens (#9960)
1 parent 6e11856 commit fd64bd7

3 files changed

Lines changed: 79 additions & 8 deletions

File tree

packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_chart.dart

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,14 @@ class _FramesChartState extends State<FramesChart> with AutoDisposeMixin {
249249
_framesScrollController = ScrollController(
250250
initialScrollOffset: initialScrollOffset,
251251
);
252+
253+
// Snap after layout so [atScrollBottom] matches for live follow.
254+
if (_selectedFrameIndex == null) {
255+
WidgetsBinding.instance.addPostFrameCallback((_) {
256+
if (!mounted || !_framesScrollController.hasClients) return;
257+
unawaited(_framesScrollController.autoScrollToBottom(jump: true));
258+
});
259+
}
252260
}
253261

254262
@override
@@ -261,13 +269,20 @@ class _FramesChartState extends State<FramesChart> with AutoDisposeMixin {
261269
}
262270

263271
double _calculateInitialHorizontalScrollOffset() {
264-
final selectedIndex = _selectedFrameIndex;
265-
if (selectedIndex == null) return 0.0;
266-
267-
final chartWidthWithoutAxisLabels =
268-
widget.constraints.maxWidth - _yAxisUnitsSpace;
272+
final chartWidthWithoutAxisLabels = math.max(
273+
0.0,
274+
widget.constraints.maxWidth - _yAxisUnitsSpace,
275+
);
269276
final totalFramesInView =
270277
chartWidthWithoutAxisLabels ~/ _defaultFrameWidthWithPadding;
278+
279+
final selectedIndex = _selectedFrameIndex;
280+
if (selectedIndex == null) {
281+
// Dock to the live edge when remounting with no selection (#9525).
282+
final framesOutOfView = widget.frames.length - totalFramesInView;
283+
return math.max(0.0, framesOutOfView * _defaultFrameWidthWithPadding);
284+
}
285+
271286
final fullFrameRangeInView = Range(0, totalFramesInView);
272287

273288
if (fullFrameRangeInView.contains(selectedIndex)) return 0.0;

packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ To learn more about DevTools, check out the
2828

2929
## Performance updates
3030

31-
TODO: Remove this section if there are not any updates.
31+
* Fixed a bug where the Flutter frames chart could appear frozen after
32+
switching away from the Performance screen and coming back.
33+
[#9960](https://github.com/flutter/devtools/pull/9960)
3234

3335
## CPU profiler updates
3436

packages/devtools_app/test/screens/performance/flutter_frames/flutter_frames_chart_test.dart

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,29 @@ void main() {
155155
expect(scrollController.offset, equals(expectedOffset));
156156
}
157157

158-
testWidgets('is zero for no selected frame', (WidgetTester tester) async {
158+
void verifyScrollAtLiveEdge(WidgetTester tester) {
159+
final scrollbar = tester.widget<Scrollbar>(find.byType(Scrollbar));
160+
final scrollController = scrollbar.controller!;
161+
expect(
162+
scrollController.offset,
163+
equals(scrollController.position.maxScrollExtent),
164+
);
165+
}
166+
167+
FlutterFrame createFrame(int number) {
168+
return FlutterFrame.fromJson({
169+
'number': number,
170+
'startTime': 10000 + number * 50000,
171+
'elapsed': 20000,
172+
'build': 10000,
173+
'raster': 12000,
174+
'vsyncOverhead': 10,
175+
});
176+
}
177+
178+
testWidgets('is at live edge for no selected frame', (
179+
WidgetTester tester,
180+
) async {
159181
expect(framesController.selectedFrame.value, isNull);
160182

161183
await pumpChart(tester);
@@ -165,7 +187,39 @@ void main() {
165187
findsNWidgets(totalFramesInView),
166188
);
167189

168-
verifyScrollOffset(tester, 0.0);
190+
verifyScrollAtLiveEdge(tester);
191+
});
192+
193+
testWidgets('stays at live edge when frames are added', (
194+
WidgetTester tester,
195+
) async {
196+
expect(framesController.selectedFrame.value, isNull);
197+
198+
await pumpChart(tester);
199+
verifyScrollAtLiveEdge(tester);
200+
201+
framesController.addFrame(createFrame(totalNumFrames));
202+
await tester.pumpAndSettle();
203+
verifyScrollAtLiveEdge(tester);
204+
});
205+
206+
testWidgets('docks to live edge after remount with no selection', (
207+
WidgetTester tester,
208+
) async {
209+
expect(framesController.selectedFrame.value, isNull);
210+
211+
await pumpChart(tester);
212+
verifyScrollAtLiveEdge(tester);
213+
214+
// Leave Performance (dispose chart), then remount after new frames.
215+
await tester.pumpWidget(wrap(const SizedBox.shrink()));
216+
await tester.pumpAndSettle();
217+
218+
framesController.addFrame(createFrame(totalNumFrames));
219+
framesController.addFrame(createFrame(totalNumFrames + 1));
220+
221+
await pumpChart(tester);
222+
verifyScrollAtLiveEdge(tester);
169223
});
170224

171225
testWidgets('is offset for selected frame', (WidgetTester tester) async {

0 commit comments

Comments
 (0)