Preserve selected feature tab in Performance page exports (#9861)

* 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

* 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.

* Add release note for performance export tab fix
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 8bed01c..591e86a 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 @@
   Future<void> _loadOfflineData(OfflinePerformanceData data) async {
     await clearData();
     offlinePerformanceData = data;
+    selectedFeatureTabIndex = data.selectedTab;
     await _applyToFeatureControllersAsync(
       (c) => c.setOfflineData(offlinePerformanceData!),
     );
@@ -285,6 +286,7 @@
       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 637eae3..a965879 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 @@
     this.frames = const <FlutterFrame>[],
     this.selectedFrame,
     this.rebuildCountModel,
+    this.selectedTab = 0,
     double? displayRefreshRate,
   }) : displayRefreshRate = displayRefreshRate ?? defaultRefreshRate;
 
@@ -36,6 +37,7 @@
       selectedFrame: selectedFrame,
       rebuildCountModel: json.rebuildCountModel,
       displayRefreshRate: json.displayRefreshRate,
+      selectedTab: json.selectedTab,
     );
   }
 
@@ -44,6 +46,7 @@
   static const displayRefreshRateKey = 'displayRefreshRate';
   static const flutterFramesKey = 'flutterFrames';
   static const selectedFrameIdKey = 'selectedFrameId';
+  static const selectedTabKey = 'selectedTab';
 
   final Uint8List? perfettoTraceBinary;
 
@@ -56,6 +59,12 @@
 
   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<String, Object?> toJson() => {
@@ -64,6 +73,7 @@
     selectedFrameIdKey: selectedFrame?.id,
     displayRefreshRateKey: displayRefreshRate,
     rebuildCountModelKey: rebuildCountModel?.toJson(),
+    selectedTabKey: selectedTab,
   };
 }
 
@@ -77,6 +87,9 @@
   int? get selectedFrameId =>
       json[OfflinePerformanceData.selectedFrameIdKey] as int?;
 
+  int get selectedTab =>
+      json[OfflinePerformanceData.selectedTabKey] as int? ?? 0;
+
   List<FlutterFrame> get frames =>
       (json[OfflinePerformanceData.flutterFramesKey] as List? ?? [])
           .cast<Map>()
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 05eef54..52096fe 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 @@
         .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/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
index 504c0bb..37c1e05 100644
--- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
+++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
@@ -27,7 +27,9 @@
 
 ## 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
 
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 84f1f06..7dade0f 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 @@
       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 @@
       expect(offlineData.selectedFrame!.id, equals(2));
       expect(offlineData.displayRefreshRate, equals(60));
       expect(offlineData.rebuildCountModel, isNull);
+      expect(offlineData.selectedTab, equals(0));
     });
 
     test('to json', () {
@@ -44,12 +46,22 @@
           OfflinePerformanceData.selectedFrameIdKey: null,
           OfflinePerformanceData.displayRefreshRateKey: 60,
           OfflinePerformanceData.rebuildCountModelKey: null,
+          OfflinePerformanceData.selectedTabKey: 0,
         }),
       );
 
       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', () {
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 88403e3..d2f0f49 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 @@
       }
     ],
     "displayRefreshRate": 60,
-    "rebuildCountModel": null
+    "rebuildCountModel": null,
+    "selectedTab": 0
   }
 }
 ''');