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