From ece7b80c08448a7b138c7a8d82b4528fc71e4229 Mon Sep 17 00:00:00 2001 From: gords2 Date: Sat, 20 Jun 2026 22:47:33 -0700 Subject: [PATCH 1/3] Preserve selected feature tab in Performance page exports The Performance page tracked the selected feature tab via `selectedFeatureTabIndex`, but it was never persisted to offline exports, so loading exported data always reset to the first tab. Mirror the Memory page's existing behavior: serialize the selected tab index into `OfflinePerformanceData` and restore it when loading offline data so the user lands on the tab they exported from. Closes #5150 --- .../screens/performance/performance_controller.dart | 2 ++ .../src/screens/performance/performance_model.dart | 13 +++++++++++++ .../screens/performance/performance_model_test.dart | 3 +++ .../lib/src/test_data/_performance_data.dart | 3 ++- 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/devtools_app/lib/src/screens/performance/performance_controller.dart b/packages/devtools_app/lib/src/screens/performance/performance_controller.dart index 8bed01cc548..591e86a46aa 100644 --- a/packages/devtools_app/lib/src/screens/performance/performance_controller.dart +++ b/packages/devtools_app/lib/src/screens/performance/performance_controller.dart @@ -185,6 +185,7 @@ class PerformanceController extends DevToolsScreenController Future _loadOfflineData(OfflinePerformanceData data) async { await clearData(); offlinePerformanceData = data; + selectedFeatureTabIndex = data.selectedTab; await _applyToFeatureControllersAsync( (c) => c.setOfflineData(offlinePerformanceData!), ); @@ -285,6 +286,7 @@ class PerformanceController extends DevToolsScreenController selectedFrame: flutterFramesController.selectedFrame.value, rebuildCountModel: rebuildCountModel, displayRefreshRate: flutterFramesController.displayRefreshRate.value, + selectedTab: selectedFeatureTabIndex, ).toJson(), ); diff --git a/packages/devtools_app/lib/src/screens/performance/performance_model.dart b/packages/devtools_app/lib/src/screens/performance/performance_model.dart index 637eae39969..a965879822c 100644 --- a/packages/devtools_app/lib/src/screens/performance/performance_model.dart +++ b/packages/devtools_app/lib/src/screens/performance/performance_model.dart @@ -18,6 +18,7 @@ class OfflinePerformanceData { this.frames = const [], this.selectedFrame, this.rebuildCountModel, + this.selectedTab = 0, double? displayRefreshRate, }) : displayRefreshRate = displayRefreshRate ?? defaultRefreshRate; @@ -36,6 +37,7 @@ class OfflinePerformanceData { selectedFrame: selectedFrame, rebuildCountModel: json.rebuildCountModel, displayRefreshRate: json.displayRefreshRate, + selectedTab: json.selectedTab, ); } @@ -44,6 +46,7 @@ class OfflinePerformanceData { static const displayRefreshRateKey = 'displayRefreshRate'; static const flutterFramesKey = 'flutterFrames'; static const selectedFrameIdKey = 'selectedFrameId'; + static const selectedTabKey = 'selectedTab'; final Uint8List? perfettoTraceBinary; @@ -56,6 +59,12 @@ class OfflinePerformanceData { final FlutterFrame? selectedFrame; + /// The index of the feature tab that was selected when the data was exported. + /// + /// This is restored when loading offline data so the user lands on the same + /// tab they exported from. + final int selectedTab; + bool get isEmpty => perfettoTraceBinary == null; Map toJson() => { @@ -64,6 +73,7 @@ class OfflinePerformanceData { selectedFrameIdKey: selectedFrame?.id, displayRefreshRateKey: displayRefreshRate, rebuildCountModelKey: rebuildCountModel?.toJson(), + selectedTabKey: selectedTab, }; } @@ -77,6 +87,9 @@ extension type _PerformanceDataJson(Map json) { int? get selectedFrameId => json[OfflinePerformanceData.selectedFrameIdKey] as int?; + int get selectedTab => + json[OfflinePerformanceData.selectedTabKey] as int? ?? 0; + List get frames => (json[OfflinePerformanceData.flutterFramesKey] as List? ?? []) .cast() diff --git a/packages/devtools_app/test/screens/performance/performance_model_test.dart b/packages/devtools_app/test/screens/performance/performance_model_test.dart index 84f1f061a98..b691eddbf89 100644 --- a/packages/devtools_app/test/screens/performance/performance_model_test.dart +++ b/packages/devtools_app/test/screens/performance/performance_model_test.dart @@ -16,6 +16,7 @@ void main() { expect(offlineData.selectedFrame, isNull); expect(offlineData.rebuildCountModel, isNull); expect(offlineData.displayRefreshRate, 60.0); + expect(offlineData.selectedTab, 0); }); test('init from parse', () { @@ -32,6 +33,7 @@ void main() { expect(offlineData.selectedFrame!.id, equals(2)); expect(offlineData.displayRefreshRate, equals(60)); expect(offlineData.rebuildCountModel, isNull); + expect(offlineData.selectedTab, equals(0)); }); test('to json', () { @@ -44,6 +46,7 @@ void main() { OfflinePerformanceData.selectedFrameIdKey: null, OfflinePerformanceData.displayRefreshRateKey: 60, OfflinePerformanceData.rebuildCountModelKey: null, + OfflinePerformanceData.selectedTabKey: 0, }), ); diff --git a/packages/devtools_test/lib/src/test_data/_performance_data.dart b/packages/devtools_test/lib/src/test_data/_performance_data.dart index 88403e32399..d2f0f490377 100644 --- a/packages/devtools_test/lib/src/test_data/_performance_data.dart +++ b/packages/devtools_test/lib/src/test_data/_performance_data.dart @@ -108244,7 +108244,8 @@ final Map samplePerformanceData = json.decode(''' } ], "displayRefreshRate": 60, - "rebuildCountModel": null + "rebuildCountModel": null, + "selectedTab": 0 } } '''); From eb0e93a1a395a09321f3ad64e25df5a8b4501892 Mon Sep 17 00:00:00 2001 From: gords2 Date: Sat, 20 Jun 2026 23:00:28 -0700 Subject: [PATCH 2/3] Honor and clamp the restored tab index when loading offline data Address review feedback on the offline tab restoration: - The restored selectedFeatureTabIndex was immediately reset to 0 by the default-feature activation in TabbedPerformanceView. Use the restored index (clamped to the available tabs) when activating the default feature instead of hardcoding 0. - The number of visible tabs can be smaller when loading offline data than when it was exported, so an out-of-range index could be passed to AnalyticsTabbedView. Clamp the index passed as initialSelectedIndex. - Add a round-trip test for a non-zero selectedTab. --- .../performance/tabbed_performance_view.dart | 16 +++++++++++++--- .../performance/performance_model_test.dart | 9 +++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/performance/tabbed_performance_view.dart b/packages/devtools_app/lib/src/screens/performance/tabbed_performance_view.dart index 05eef54cd1c..52096fe6517 100644 --- a/packages/devtools_app/lib/src/screens/performance/tabbed_performance_view.dart +++ b/packages/devtools_app/lib/src/screens/performance/tabbed_performance_view.dart @@ -80,17 +80,27 @@ class _TabbedPerformanceViewState extends State .map((t) => t.featureController) .toList(); - // If there is not an active feature, activate the first. + // The set of visible tabs can differ between when offline data was exported + // and when it is loaded (e.g. the Frame Analysis and Rebuild Stats tabs are + // only shown for Flutter apps with the relevant data). Clamp the restored + // tab index to the tabs that are actually available to avoid an + // out-of-bounds selection. + final selectedTabIndex = controller.selectedFeatureTabIndex.clamp( + 0, + tabs.length - 1, + ); + + // If there is not an active feature, activate the selected one. if (featureControllers.firstWhereOrNull( (controller) => controller?.isActiveFeature ?? false, ) == null) { - _setActiveFeature(0, featureControllers[0]); + _setActiveFeature(selectedTabIndex, featureControllers[selectedTabIndex]); } return AnalyticsTabbedView( tabs: tabs, - initialSelectedIndex: controller.selectedFeatureTabIndex, + initialSelectedIndex: selectedTabIndex, gaScreen: gac.performance, onTabChanged: (int index) { _setActiveFeature(index, featureControllers[index]); diff --git a/packages/devtools_app/test/screens/performance/performance_model_test.dart b/packages/devtools_app/test/screens/performance/performance_model_test.dart index b691eddbf89..7dade0fd72f 100644 --- a/packages/devtools_app/test/screens/performance/performance_model_test.dart +++ b/packages/devtools_app/test/screens/performance/performance_model_test.dart @@ -53,6 +53,15 @@ void main() { offlineData = OfflinePerformanceData.fromJson(rawPerformanceData); expect(offlineData.toJson(), rawPerformanceData); }); + + test('round trips a non-zero selectedTab', () { + final offlineData = OfflinePerformanceData(selectedTab: 2); + final json = offlineData.toJson(); + expect(json[OfflinePerformanceData.selectedTabKey], equals(2)); + + final parsed = OfflinePerformanceData.fromJson(json); + expect(parsed.selectedTab, equals(2)); + }); }); group('$FlutterTimelineEvent', () { From 06ae2cf57134a55a856e6a4f37e8490e6d29ae25 Mon Sep 17 00:00:00 2001 From: gords2 Date: Mon, 22 Jun 2026 17:49:39 -0700 Subject: [PATCH 3/3] Add release note for performance export tab fix --- 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 2cd2c928fe0..b028146eb4f 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 selected feature tab was not restored when loading + exported Performance data. - + [#9861](https://github.com/flutter/devtools/pull/9861) ## CPU profiler updates