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 } } ''');