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..4e1303cc957 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 @@ -129,6 +129,10 @@ class TimelineEventsController extends PerformanceFeatureController RateLimiter? _timelinePollingRateLimiter; + /// Whether the connected app uses a timeline recorder, such as systrace, + /// that does not store timeline events in the VM. + bool _vmTimelineUnavailable = false; + @override Future init() async { perfettoController.init(); @@ -155,10 +159,11 @@ class TimelineEventsController extends PerformanceFeatureController // Load available timeline events. await forceRefresh(); + if (_vmTimelineUnavailable) return; _timelinePollingRateLimiter = RateLimiter( _timelinePollingRateLimit, - _pullPerfettoVmTimeline, + _maybePullPerfettoVmTimeline, ); // Poll for new timeline events. @@ -170,6 +175,24 @@ class TimelineEventsController extends PerformanceFeatureController }); } + /// Pulls the VM timeline, unless the VM has already reported that it does + /// not store timeline events for the connected app's recorder. + /// + /// The VM answers with [RPCErrorKind.kInvalidTimelineRequest] for recorders + /// such as systrace, so that error is handled here instead of failing + /// [init]. Any other error is rethrown. + Future _maybePullPerfettoVmTimeline() async { + if (_vmTimelineUnavailable) return; + try { + await _pullPerfettoVmTimeline(); + } on RPCError catch (e) { + if (e.code != RPCErrorKind.kInvalidTimelineRequest.code) rethrow; + _vmTimelineUnavailable = true; + _log.info(e.details ?? e.message); + pushVmTimelineUnavailableWarning(); + } + } + Future _pullPerfettoVmTimeline({bool isInitialPull = false}) async { final service = serviceConnection.serviceManager.service; if (service == null) return; @@ -337,7 +360,7 @@ class TimelineEventsController extends PerformanceFeatureController Future _forceRefresh() async { debugTraceCallback(() => _log.info('[forceRefresh]')); - await _pullPerfettoVmTimeline(); + await _maybePullPerfettoVmTimeline(); processTrackEvents(); await loadPerfettoTrace(); } @@ -491,7 +514,7 @@ class TimelineEventsController extends PerformanceFeatureController } void _maybePushNoTimelineEventsWarning() { - if (isActiveFeature) { + if (isActiveFeature && !_vmTimelineUnavailable) { pushNoTimelineEventsAvailableWarning(); } } diff --git a/packages/devtools_app/lib/src/screens/performance/performance_utils.dart b/packages/devtools_app/lib/src/screens/performance/performance_utils.dart index 835da156156..c676f1a768e 100644 --- a/packages/devtools_app/lib/src/screens/performance/performance_utils.dart +++ b/packages/devtools_app/lib/src/screens/performance/performance_utils.dart @@ -17,3 +17,14 @@ void pushNoTimelineEventsAvailableWarning() { 'To avoid this, open the DevTools Performance page earlier.', ); } + +/// Warns that timeline events are unavailable because the connected app uses +/// a timeline recorder that does not store events in the Dart VM. +void pushVmTimelineUnavailableWarning() { + notificationService.push( + 'Timeline events are not available because the app uses a timeline ' + 'recorder, such as systrace, that does not store events in the Dart VM. ' + 'To see timeline events, restart the app without system tracing enabled.', + isDismissible: true, + ); +} diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 7d47c6b982a..e81626c873c 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -24,7 +24,9 @@ TODO: Remove this section if there are not any updates. ## Performance updates -TODO: Remove this section if there are not any updates. +* Fixed the Performance page loading forever when the connected app uses a + timeline recorder that DevTools cannot read from, such as systrace. + [#6524](https://github.com/flutter/devtools/issues/6524) ## CPU profiler updates 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..f0beb23ab07 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 @@ -10,6 +10,7 @@ import 'package:devtools_app_shared/utils.dart'; import 'package:devtools_test/devtools_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; +import 'package:vm_service/vm_service.dart'; import '../../../test_infra/test_data/performance/sample_performance_data.dart'; @@ -109,5 +110,43 @@ void main() { expect(eventsController.fullPerfettoTrace, isEmpty); }); }); + + test('init completes when the VM timeline is unavailable', () async { + final originalService = fakeServiceManager.serviceManager.service; + addTearDown( + () => fakeServiceManager.serviceManager.service = originalService, + ); + final service = MockVmServiceWrapper(); + fakeServiceManager.serviceManager.service = service; + when(service.setVMTimelineFlags(any)).thenAnswer((_) async => Success()); + when( + service.getVMTimelineMicros(), + ).thenAnswer((_) async => Timestamp(timestamp: 0)); + // The VM rejects timeline requests while the systrace recorder is used. + when( + service.getPerfettoVMTimeline( + timeOriginMicros: anyNamed('timeOriginMicros'), + timeExtentMicros: anyNamed('timeExtentMicros'), + ), + ).thenAnswer( + (_) async => throw RPCError( + 'getPerfettoVMTimeline', + RPCErrorKind.kInvalidTimelineRequest.code, + ), + ); + setGlobal(PreferencesController, PreferencesController()); + setGlobal(NotificationService, NotificationService()); + addTearDown(eventsController.dispose); + + await eventsController.init(); + await eventsController.forceRefresh(); + + expect(eventsController.status.value, EventsControllerStatus.ready); + verify(service.getVMTimelineMicros()).called(1); + expect( + notificationService.activeMessages.single.text, + contains('systrace'), + ); + }); }); }