From 42022684c026ce5c7cb79bbfa6c524398e99d290 Mon Sep 17 00:00:00 2001 From: khanak0509 Date: Tue, 20 Oct 2026 02:22:34 +0530 Subject: [PATCH] Fetch latest timeline events before exporting Performance data --- .../panes/controls/performance_controls.dart | 5 +++- .../timeline_events_controller.dart | 12 +++++++++ .../performance/performance_controller.dart | 14 ++++++++++ .../performance_controller_test.dart | 12 +++++++++ .../timeline_events_controller_test.dart | 26 +++++++++++++++++++ 5 files changed, 68 insertions(+), 1 deletion(-) diff --git a/packages/devtools_app/lib/src/screens/performance/panes/controls/performance_controls.dart b/packages/devtools_app/lib/src/screens/performance/panes/controls/performance_controls.dart index d1d5ba807c9..6da95046cd0 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/controls/performance_controls.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/controls/performance_controls.dart @@ -148,7 +148,10 @@ class _SecondaryPerformanceControls extends StatelessWidget { const SizedBox(width: denseSpacing), OpenSaveButtonGroup( screenId: ScreenMetaData.performance.id, - onSave: (_) => controller.exportData(), + onSave: (_) async { + await controller.fetchFullDataBeforeExport(); + controller.exportData(); + }, ), if (isFlutterApp) ...[ const SizedBox(width: denseSpacing), diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_controller.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_controller.dart index c37291140d6..fb0aa8a8e10 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_controller.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/timeline_events_controller.dart @@ -342,6 +342,18 @@ class TimelineEventsController extends PerformanceFeatureController await loadPerfettoTrace(); } + /// Pulls any timeline events from the VM that have not been received yet. + /// + /// Unlike [forceRefresh], this does not reload the Perfetto trace viewer. + Future pullLatestTimelineEvents() async { + try { + await _pullPerfettoVmTimeline(); + } catch (e, st) { + // Don't block the export of the data we already have. + _log.warning('Failed to pull the latest timeline events.', e, st); + } + } + void processTrackEvents() { if (!_isFlutterAppHelper()) { debugTraceCallback( 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 b9e158009c5..bc4d295efd0 100644 --- a/packages/devtools_app/lib/src/screens/performance/performance_controller.dart +++ b/packages/devtools_app/lib/src/screens/performance/performance_controller.dart @@ -291,6 +291,20 @@ class PerformanceController extends DevToolsScreenController ).toJson(), ); + /// Pulls the latest timeline events so that they are included in the export. + /// + /// Timeline events are only polled periodically, so the most recent events + /// would otherwise be missing from the export. + Future fetchFullDataBeforeExport() async { + final serviceManager = serviceConnection.serviceManager; + if (offlineDataController.showingOfflineData.value || + !serviceManager.connectedState.value.connected || + (serviceManager.connectedApp?.isDartWebAppNow ?? false)) { + return; + } + await timelineEventsController.pullLatestTimelineEvents(); + } + @override FutureOr releaseMemory({bool partial = false}) async { await clearData(partial: partial, clearVmTimeline: !partial); diff --git a/packages/devtools_app/test/screens/performance/performance_controller_test.dart b/packages/devtools_app/test/screens/performance/performance_controller_test.dart index b33f7381334..d239790cf27 100644 --- a/packages/devtools_app/test/screens/performance/performance_controller_test.dart +++ b/packages/devtools_app/test/screens/performance/performance_controller_test.dart @@ -49,5 +49,17 @@ void main() { expect(controller.flutterFramesController.isActiveFeature, isTrue); expect(controller.timelineEventsController.isActiveFeature, isTrue); }); + + test('fetchFullDataBeforeExport does not pull timeline events when showing ' + 'offline data', () async { + final mockServiceManager = + mockServiceConnection.serviceManager as MockServiceManager; + clearInteractions(mockServiceManager); + + await controller.fetchFullDataBeforeExport(); + + verifyNever(mockServiceManager.service); + expect(controller.timelineEventsController.fullPerfettoTrace, isEmpty); + }); }); } diff --git a/packages/devtools_app/test/screens/performance/timeline_events/timeline_events_controller_test.dart b/packages/devtools_app/test/screens/performance/timeline_events/timeline_events_controller_test.dart index 740325d17c5..38831c3c10b 100644 --- a/packages/devtools_app/test/screens/performance/timeline_events/timeline_events_controller_test.dart +++ b/packages/devtools_app/test/screens/performance/timeline_events/timeline_events_controller_test.dart @@ -82,6 +82,32 @@ void main() { ); }); + group('pullLatestTimelineEvents', () { + setUp(() { + setGlobal(PreferencesController, PreferencesController()); + }); + + test('adds the latest VM timeline to the trace', () async { + expect(eventsController.fullPerfettoTrace, isEmpty); + + await eventsController.pullLatestTimelineEvents(); + + expect(eventsController.fullPerfettoTrace, isNotEmpty); + }); + + test('completes without throwing when the pull fails', () async { + // This fake service throws when the VM timeline is requested because + // it was not provided any timeline data. + setGlobal(ServiceConnectionManager, FakeServiceConnectionManager()); + + await expectLater( + eventsController.pullLatestTimelineEvents(), + completes, + ); + expect(eventsController.fullPerfettoTrace, isEmpty); + }); + }); + group('can clearData', () { setUp(() async { // Set some data in the controller. It does not matter if it is connected