From 1a352973e8cc697b657d806eaba868f47e921727 Mon Sep 17 00:00:00 2001 From: crackedhandle Date: Fri, 3 Apr 2026 05:56:10 +0530 Subject: [PATCH 1/2] Show debug-mode message for widget rebuilds in profile mode When running in profile mode, the Performance panel showed a disabled 'Count widget builds' checkbox which was misleading. Widget rebuild counts rely on debugOnRebuildDirtyWidget which is only available in debug mode. This change: - Removes the checkbox when running in profile mode - Shows a clear message: 'Rebuild information is not available for this frame. Widget rebuild counts are only available when running an app in debug-mode.' - Adds widget tests for both profile mode and debug mode behavior Fixes #9730 --- .../panes/rebuild_stats/rebuild_stats.dart | 13 +++ .../rebuild_stats_view_test.dart | 93 +++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 packages/devtools_app/test/screens/performance/rebuild_stats/rebuild_stats_view_test.dart diff --git a/packages/devtools_app/lib/src/screens/performance/panes/rebuild_stats/rebuild_stats.dart b/packages/devtools_app/lib/src/screens/performance/panes/rebuild_stats/rebuild_stats.dart index a19d2502276..41e64aae318 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/rebuild_stats/rebuild_stats.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/rebuild_stats/rebuild_stats.dart @@ -92,6 +92,19 @@ class _RebuildStatsViewState extends State @override Widget build(BuildContext context) { + final isProfileBuild = + serviceConnection.serviceManager.connectedApp?.isProfileBuildNow ?? + false; + if (isProfileBuild) { + return const Center( + child: Text( + 'Rebuild information is not available for this frame.\n' + 'Widget rebuild counts are only available when running ' + 'an app in debug-mode.', + textAlign: TextAlign.center, + ), + ); + } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/packages/devtools_app/test/screens/performance/rebuild_stats/rebuild_stats_view_test.dart b/packages/devtools_app/test/screens/performance/rebuild_stats/rebuild_stats_view_test.dart new file mode 100644 index 00000000000..23d10c2bbe9 --- /dev/null +++ b/packages/devtools_app/test/screens/performance/rebuild_stats/rebuild_stats_view_test.dart @@ -0,0 +1,93 @@ +// Copyright 2026 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +import 'dart:async'; + +import 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_app/src/screens/performance/panes/rebuild_stats/rebuild_stats.dart'; +import 'package:devtools_app/src/screens/performance/panes/rebuild_stats/rebuild_stats_model.dart'; +import 'package:devtools_app/src/screens/performance/panes/flutter_frames/flutter_frame_model.dart'; +import 'package:devtools_app_shared/service.dart'; +import 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_app_shared/ui.dart'; +import 'package:devtools_app_shared/utils.dart'; +import 'package:devtools_test/devtools_test.dart'; +import 'package:devtools_test/helpers.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +void main() { + group('RebuildStatsView', () { + late FakeServiceConnectionManager fakeServiceConnection; + late RebuildCountModel model; + late ValueNotifier selectedFrame; + + setUp(() { + fakeServiceConnection = FakeServiceConnectionManager(); + final app = fakeServiceConnection.serviceManager.connectedApp!; + when(app.initialized).thenReturn(Completer()..complete(true)); + when(app.isDartWebAppNow).thenReturn(false); + when(app.isFlutterAppNow).thenReturn(true); + when(app.isDartCliAppNow).thenReturn(false); + when(app.isDartWebApp).thenAnswer((_) async => false); + when(app.isProfileBuild).thenAnswer((_) async => false); + setGlobal(ServiceConnectionManager, fakeServiceConnection); + setGlobal(IdeTheme, IdeTheme()); + setGlobal(NotificationService, NotificationService()); + setGlobal(BannerMessagesController, BannerMessagesController()); + setGlobal(PreferencesController, PreferencesController()); + setGlobal(OfflineDataController, OfflineDataController()); + model = RebuildCountModel(); + selectedFrame = ValueNotifier(null); + }); + + testWidgets( + 'shows message when running in profile mode', + (WidgetTester tester) async { + final app = fakeServiceConnection.serviceManager.connectedApp!; + when(app.isProfileBuildNow).thenReturn(true); + + await tester.pumpWidget( + wrapWithControllers( + RebuildStatsView( + model: model, + selectedFrame: selectedFrame, + ), + ), + ); + await tester.pump(); + + expect( + find.textContaining('Widget rebuild counts are only available'), + findsOneWidget, + ); + }, + ); + + testWidgets( + 'shows normal UI when running in debug mode', + (WidgetTester tester) async { + final app = fakeServiceConnection.serviceManager.connectedApp!; + when(app.isProfileBuildNow).thenReturn(false); + + await tester.pumpWidget( + wrapWithControllers( + RebuildStatsView( + model: model, + selectedFrame: selectedFrame, + ), + ), + ); + await tester.pump(); + + expect( + find.textContaining('Widget rebuild counts are only available'), + findsNothing, + ); + }, + ); + }); +} From 44942d2fbaa526cba9ad48e88ab5c3f8a723aecc Mon Sep 17 00:00:00 2001 From: crackedhandle Date: Wed, 8 Apr 2026 20:29:25 +0530 Subject: [PATCH 2/2] Move RebuildStatsView tests to performance_screen_test Per reviewer feedback, moved the profile mode widget tests from a separate test file into the existing performance_screen_test.dart. Also added required imports for RebuildStatsView, RebuildCountModel, FlutterFrame and foundation. --- .../performance/performance_screen_test.dart | 122 +++++++++++------- .../rebuild_stats_view_test.dart | 93 ------------- 2 files changed, 76 insertions(+), 139 deletions(-) delete mode 100644 packages/devtools_app/test/screens/performance/rebuild_stats/rebuild_stats_view_test.dart diff --git a/packages/devtools_app/test/screens/performance/performance_screen_test.dart b/packages/devtools_app/test/screens/performance/performance_screen_test.dart index 6f23218ee51..14752cb9565 100644 --- a/packages/devtools_app/test/screens/performance/performance_screen_test.dart +++ b/packages/devtools_app/test/screens/performance/performance_screen_test.dart @@ -9,6 +9,9 @@ import 'dart:async'; import 'package:devtools_app/devtools_app.dart'; import 'package:devtools_app/src/screens/performance/panes/controls/performance_controls.dart'; +import 'package:devtools_app/src/screens/performance/panes/flutter_frames/flutter_frame_model.dart'; +import 'package:devtools_app/src/screens/performance/panes/rebuild_stats/rebuild_stats.dart'; +import 'package:devtools_app/src/screens/performance/panes/rebuild_stats/rebuild_stats_model.dart'; import 'package:devtools_app/src/screens/performance/panes/timeline_events/timeline_events_view.dart'; import 'package:devtools_app/src/screens/performance/tabbed_performance_view.dart'; import 'package:devtools_app/src/shared/feature_flags.dart'; @@ -18,6 +21,7 @@ import 'package:devtools_app_shared/utils.dart'; import 'package:devtools_shared/devtools_test_utils.dart'; import 'package:devtools_test/devtools_test.dart'; import 'package:devtools_test/helpers.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -153,7 +157,6 @@ void main() { findsOneWidget, ); - // Make sure NO Flutter-specific information is included: expect( markdownFinder('The Flutter framework emits timeline events'), findsNothing, @@ -183,7 +186,6 @@ void main() { findsOneWidget, ); - // Make sure Flutter-specific information is included: expect( markdownFinder('The Flutter framework emits timeline events'), findsOneWidget, @@ -222,7 +224,6 @@ void main() { final chartButtonFinder = find.byType(VisibilityButton); expect(chartButtonFinder, findsOneWidget); - // The flutter frames chart is visible. expect(find.byType(FramesChartControls), findsOneWidget); expect( preferences.performance.showFlutterFramesChart.value, @@ -232,7 +233,6 @@ void main() { await tester.tap(chartButtonFinder); await tester.pumpAndSettle(); - // The flutter frames chart should no longer be visible. expect(find.byType(FramesChartControls), findsNothing); expect( preferences.performance.showFlutterFramesChart.value, @@ -242,7 +242,6 @@ void main() { await tester.tap(chartButtonFinder); await tester.pumpAndSettle(); - // The flutter frames chart should be visible again. expect(find.byType(FramesChartControls), findsOneWidget); expect( preferences.performance.showFlutterFramesChart.value, @@ -252,46 +251,6 @@ void main() { }, ); - // testWidgetsWithWindowSize( - // 'clears timeline on clear', - // windowSize, - // (WidgetTester tester) async { - // await tester.runAsync(() async { - // await pumpPerformanceScreen(tester, runAsync: true); - // await tester.pumpAndSettle(); - - // // Ensure the Timeline Events tab is selected. - // final timelineEventsTabFinder = find.text('Timeline Events'); - // expect(timelineEventsTabFinder, findsOneWidget); - // await tester.tap(timelineEventsTabFinder); - // await tester.pumpAndSettle(); - - // expect( - // controller.timelineEventsController.allTraceEvents, - // isNotEmpty, - // ); - // expect(find.byType(FlutterFramesChart), findsOneWidget); - // expect(find.byType(TimelineFlameChart), findsOneWidget); - // expect( - // find.byKey(TimelineEventsView.emptyTimelineKey), - // findsNothing, - // ); - // expect(find.byType(EventDetails), findsOneWidget); - - // await tester.tap(find.byIcon(Icons.block)); - // await tester.pumpAndSettle(); - // expect(controller.timelineEventsController.allTraceEvents, isEmpty); - // expect(find.byType(FlutterFramesChart), findsOneWidget); - // expect(find.byType(TimelineFlameChart), findsNothing); - // expect( - // find.byKey(TimelineEventsView.emptyTimelineKey), - // findsOneWidget, - // ); - // expect(find.byType(EventDetails), findsNothing); - // }); - // }, - // ); - testWidgetsWithWindowSize('opens enhance tracing overlay', windowSize, ( WidgetTester tester, ) async { @@ -395,9 +354,80 @@ void main() { }, ); }); + + group('RebuildStatsView', () { + late FakeServiceConnectionManager fakeServiceConnection; + late RebuildCountModel model; + late ValueNotifier selectedFrame; + + setUp(() { + fakeServiceConnection = FakeServiceConnectionManager(); + final app = fakeServiceConnection.serviceManager.connectedApp!; + when(app.initialized).thenReturn(Completer()..complete(true)); + when(app.isDartWebAppNow).thenReturn(false); + when(app.isFlutterAppNow).thenReturn(true); + when(app.isDartCliAppNow).thenReturn(false); + when(app.isDartWebApp).thenAnswer((_) async => false); + when(app.isProfileBuild).thenAnswer((_) async => false); + setGlobal(ServiceConnectionManager, fakeServiceConnection); + setGlobal(IdeTheme, IdeTheme()); + setGlobal(NotificationService, NotificationService()); + setGlobal(BannerMessagesController, BannerMessagesController()); + setGlobal(PreferencesController, PreferencesController()); + setGlobal(OfflineDataController, OfflineDataController()); + model = RebuildCountModel(); + selectedFrame = ValueNotifier(null); + }); + + testWidgets( + 'shows message when running in profile mode', + (WidgetTester tester) async { + final app = fakeServiceConnection.serviceManager.connectedApp!; + when(app.isProfileBuildNow).thenReturn(true); + + await tester.pumpWidget( + wrapWithControllers( + RebuildStatsView( + model: model, + selectedFrame: selectedFrame, + ), + ), + ); + await tester.pump(); + + expect( + find.textContaining('Widget rebuild counts are only available'), + findsOneWidget, + ); + }, + ); + + testWidgets( + 'shows normal UI when running in debug mode', + (WidgetTester tester) async { + final app = fakeServiceConnection.serviceManager.connectedApp!; + when(app.isProfileBuildNow).thenReturn(false); + + await tester.pumpWidget( + wrapWithControllers( + RebuildStatsView( + model: model, + selectedFrame: selectedFrame, + ), + ), + ); + await tester.pump(); + + expect( + find.textContaining('Widget rebuild counts are only available'), + findsNothing, + ); + }, + ); + }); }); } Finder markdownFinder(String textMatch) => find.byWidgetPredicate( (widget) => widget is Markdown && widget.data.contains(textMatch), -); +); \ No newline at end of file diff --git a/packages/devtools_app/test/screens/performance/rebuild_stats/rebuild_stats_view_test.dart b/packages/devtools_app/test/screens/performance/rebuild_stats/rebuild_stats_view_test.dart deleted file mode 100644 index 23d10c2bbe9..00000000000 --- a/packages/devtools_app/test/screens/performance/rebuild_stats/rebuild_stats_view_test.dart +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2026 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'dart:async'; - -import 'package:devtools_app/devtools_app.dart'; -import 'package:devtools_app/src/screens/performance/panes/rebuild_stats/rebuild_stats.dart'; -import 'package:devtools_app/src/screens/performance/panes/rebuild_stats/rebuild_stats_model.dart'; -import 'package:devtools_app/src/screens/performance/panes/flutter_frames/flutter_frame_model.dart'; -import 'package:devtools_app_shared/service.dart'; -import 'package:devtools_app/devtools_app.dart'; -import 'package:devtools_app_shared/ui.dart'; -import 'package:devtools_app_shared/utils.dart'; -import 'package:devtools_test/devtools_test.dart'; -import 'package:devtools_test/helpers.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; - -void main() { - group('RebuildStatsView', () { - late FakeServiceConnectionManager fakeServiceConnection; - late RebuildCountModel model; - late ValueNotifier selectedFrame; - - setUp(() { - fakeServiceConnection = FakeServiceConnectionManager(); - final app = fakeServiceConnection.serviceManager.connectedApp!; - when(app.initialized).thenReturn(Completer()..complete(true)); - when(app.isDartWebAppNow).thenReturn(false); - when(app.isFlutterAppNow).thenReturn(true); - when(app.isDartCliAppNow).thenReturn(false); - when(app.isDartWebApp).thenAnswer((_) async => false); - when(app.isProfileBuild).thenAnswer((_) async => false); - setGlobal(ServiceConnectionManager, fakeServiceConnection); - setGlobal(IdeTheme, IdeTheme()); - setGlobal(NotificationService, NotificationService()); - setGlobal(BannerMessagesController, BannerMessagesController()); - setGlobal(PreferencesController, PreferencesController()); - setGlobal(OfflineDataController, OfflineDataController()); - model = RebuildCountModel(); - selectedFrame = ValueNotifier(null); - }); - - testWidgets( - 'shows message when running in profile mode', - (WidgetTester tester) async { - final app = fakeServiceConnection.serviceManager.connectedApp!; - when(app.isProfileBuildNow).thenReturn(true); - - await tester.pumpWidget( - wrapWithControllers( - RebuildStatsView( - model: model, - selectedFrame: selectedFrame, - ), - ), - ); - await tester.pump(); - - expect( - find.textContaining('Widget rebuild counts are only available'), - findsOneWidget, - ); - }, - ); - - testWidgets( - 'shows normal UI when running in debug mode', - (WidgetTester tester) async { - final app = fakeServiceConnection.serviceManager.connectedApp!; - when(app.isProfileBuildNow).thenReturn(false); - - await tester.pumpWidget( - wrapWithControllers( - RebuildStatsView( - model: model, - selectedFrame: selectedFrame, - ), - ), - ); - await tester.pump(); - - expect( - find.textContaining('Widget rebuild counts are only available'), - findsNothing, - ); - }, - ); - }); -}