diff --git a/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_controller.dart b/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_controller.dart index b7b06f8f6d6..b613b0445f4 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_controller.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_controller.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:math' as math; import 'package:collection/collection.dart'; @@ -30,8 +31,10 @@ class FlutterFramesController extends PerformanceFeatureController { /// Whether we should show the Flutter frames chart. ValueListenable get showFlutterFramesChart => _showFlutterFramesChart; final _showFlutterFramesChart = ValueNotifier(true); - void toggleShowFlutterFrames(bool value) => - _showFlutterFramesChart.value = value; + void toggleShowFlutterFrames(bool value) { + _showFlutterFramesChart.value = value; + unawaited(setIsActiveFeature(_showFlutterFramesChart.value)); + } /// Whether flutter frames are currently being recorded. ValueListenable get recordingFrames => _recordingFrames; @@ -82,6 +85,17 @@ class FlutterFramesController extends PerformanceFeatureController { data?.displayRefreshRate = _displayRefreshRate.value; } } + await setIsActiveFeature(true); + } + + // We override this for [FlutterFramesController] because this feature's + // "active" state will be determined by different parameters from other + // feature controllers, which respond to tab switches. + @override + Future setIsActiveFeature(bool value) async { + final isFlutterApp = serviceManager.connectedApp?.isFlutterAppNow ?? false; + value = isFlutterApp && _showFlutterFramesChart.value; + await super.setIsActiveFeature(value); } void addFrame(FlutterFrame frame) { @@ -128,7 +142,11 @@ class FlutterFramesController extends PerformanceFeatureController { Future toggleSelectedFrame(FlutterFrame frame) async { handleSelectedFrame(frame); - performanceController.timelineEventsController.handleSelectedFrame(frame); + // We do not need to block the UI on the TimelineEvents feature loading the + // selected frame. + unawaited( + performanceController.timelineEventsController.handleSelectedFrame(frame), + ); } void _addPendingFlutterFrames() { diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_desktop.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_desktop.dart index 8544adfd657..4bc0a9194b8 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_desktop.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_desktop.dart @@ -4,12 +4,19 @@ import '../../../../../primitives/trace_event.dart'; import '../../../../../primitives/utils.dart'; +import '../../../performance_controller.dart'; class PerfettoController { + PerfettoController(this.performanceController); + + final PerformanceController performanceController; + void init() {} void dispose() {} + Future onBecomingActive() async {} + Future loadTrace(List devToolsTraceEvents) async {} Future scrollToTimeRange(TimeRange timeRange) async {} diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart index d6c35578ade..b0d6d00bd60 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/_perfetto_controller_web.dart @@ -12,6 +12,7 @@ import '../../../../../primitives/auto_dispose.dart'; import '../../../../../primitives/trace_event.dart'; import '../../../../../primitives/utils.dart'; import '../../../../../shared/globals.dart'; +import '../../../performance_controller.dart'; /// Flag to enable embedding an instance of the Perfetto UI running on /// localhost. @@ -24,6 +25,10 @@ const _debugUseLocalPerfetto = false; class PerfettoController extends DisposableController with AutoDisposeControllerMixin { + PerfettoController(this.performanceController); + + final PerformanceController performanceController; + static const viewId = 'embedded-perfetto'; /// Url when running Perfetto locally following the instructions here: @@ -82,6 +87,19 @@ class PerfettoController extends DisposableController late final Completer _devtoolsThemeHandlerReady; + /// Trace events that we should load, but have not yet since the trace viewer + /// is not visible (i.e. [TimelineEventsController.isActiveFeature] is false). + List? pendingTraceEventsToLoad; + + /// Time range we should scroll to, but have not yet since the trace viewer + /// is not visible (i.e. [TimelineEventsController.isActiveFeature] is false). + TimeRange? pendingScrollToTimeRange; + + /// Boolean value representing the pending theme change we that we should + /// apply, but have not yet since the trace viewer is not visible (i.e. + /// [TimelineEventsController.isActiveFeature] is false). + bool? pendingLoadDarkMode; + void init() { _perfettoReady = Completer(); _devtoolsThemeHandlerReady = Completer(); @@ -105,14 +123,34 @@ class PerfettoController extends DisposableController html.window.addEventListener('message', _handleMessage); if (isExternalBuild) { - unawaited(_loadInitialStyle()); + unawaited(_loadStyle(preferences.darkModeTheme.value)); addAutoDisposeListener(preferences.darkModeTheme, () async { - _loadStyle(preferences.darkModeTheme.value); + await _loadStyle(preferences.darkModeTheme.value); }); } } + Future onBecomingActive() async { + if (pendingLoadDarkMode != null) { + await _loadStyle(pendingLoadDarkMode!); + } + if (pendingTraceEventsToLoad != null) { + await loadTrace(pendingTraceEventsToLoad!); + pendingTraceEventsToLoad = null; + } + if (pendingScrollToTimeRange != null) { + await scrollToTimeRange(pendingScrollToTimeRange!); + pendingScrollToTimeRange = null; + } + } + Future loadTrace(List devToolsTraceEvents) async { + if (!performanceController.timelineEventsController.isActiveFeature) { + pendingTraceEventsToLoad = List.from(devToolsTraceEvents); + return; + } + pendingTraceEventsToLoad = null; + await _pingPerfettoUntilReady(); final encodedJson = jsonEncode({ @@ -132,6 +170,12 @@ class PerfettoController extends DisposableController } Future scrollToTimeRange(TimeRange timeRange) async { + if (!performanceController.timelineEventsController.isActiveFeature) { + pendingScrollToTimeRange = timeRange; + return; + } + pendingScrollToTimeRange = null; + if (!timeRange.isWellFormed) { notificationService.push( 'No timeline events available for the selected frame. Timeline ' @@ -152,16 +196,17 @@ class PerfettoController extends DisposableController }); } - Future _loadInitialStyle() async { + Future _loadStyle(bool darkMode) async { if (!isExternalBuild) return; - await _pingDevToolsThemeHandlerUntilReady(); - _loadStyle(preferences.darkModeTheme.value); - } + if (!performanceController.timelineEventsController.isActiveFeature) { + pendingLoadDarkMode = darkMode; + return; + } + pendingLoadDarkMode = null; - void _loadStyle(bool darkMode) { - if (!isExternalBuild) return; // This message will be handled by [devtools_theme_handler.js], which is // included in the Perfetto build inside [packages/perfetto_compiled/dist]. + await _pingDevToolsThemeHandlerUntilReady(); _postMessageWithId( _devtoolsThemeChange, perfettoIgnore: true, diff --git a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/perfetto.dart b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/perfetto.dart index 172e84e9ce1..0d6721962d1 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/perfetto.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/timeline_events/perfetto/perfetto.dart @@ -8,8 +8,6 @@ import '_perfetto_controller_desktop.dart' if (dart.library.html) '_perfetto_controller_web.dart'; import '_perfetto_desktop.dart' if (dart.library.html) '_perfetto_web.dart'; -PerfettoController createPerfettoController() => PerfettoController(); - class EmbeddedPerfetto extends StatelessWidget { const EmbeddedPerfetto({Key? key, required this.perfettoController}) : super(key: key); 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 c4ea40efb37..7a5920f780a 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 @@ -29,7 +29,8 @@ import '../../performance_screen.dart'; import '../../performance_utils.dart'; import '../../simple_trace_example.dart'; import '../flutter_frames/flutter_frame_model.dart'; -import 'perfetto/perfetto.dart'; +import 'perfetto/_perfetto_controller_desktop.dart' + if (dart.library.html) 'perfetto/_perfetto_controller_web.dart'; import 'timeline_event_processor.dart'; /// Debugging flag to load sample trace events from [simple_trace_example.dart]. @@ -39,6 +40,7 @@ class TimelineEventsController extends PerformanceFeatureController with AutoDisposeControllerMixin { TimelineEventsController(super.performanceController) { legacyController = LegacyTimelineEventsController(performanceController); + perfettoController = PerfettoController(performanceController); } /// Controller that contains business logic for the legacy trace viewer. @@ -46,7 +48,10 @@ class TimelineEventsController extends PerformanceFeatureController /// This controller will be used when [useLegacyTraceViewer.value] is true. late final LegacyTimelineEventsController legacyController; - final perfettoController = createPerfettoController(); + /// Controller that contains business logic for the Perfetto trace viewer. + /// + /// This controller will be used when [useLegacyTraceViewer.value] is false. + late final PerfettoController perfettoController; /// Trace events in the current timeline. /// @@ -65,6 +70,9 @@ class TimelineEventsController extends PerformanceFeatureController final useLegacyTraceViewer = ValueNotifier(!FeatureFlags.embeddedPerfetto || !kIsWeb); + bool get _perfettoMode => + FeatureFlags.embeddedPerfetto && !useLegacyTraceViewer.value; + /// Whether the recorded timeline data is currently being processed. ValueListenable get processing => _processing; final _processing = ValueNotifier(false); @@ -108,6 +116,14 @@ class TimelineEventsController extends PerformanceFeatureController } } + @override + Future onBecomingActive() async { + if (_perfettoMode) { + await perfettoController.onBecomingActive(); + } + await super.onBecomingActive(); + } + Future _initForServiceConnection() async { legacyController.init(); await serviceManager.timelineStreamManager.setDefaultTimelineStreams(); @@ -123,9 +139,7 @@ class TimelineEventsController extends PerformanceFeatureController // Load available timeline events. await _pullTraceEventsFromVmTimeline(isInitialPull: true); - _processing.value = true; - await processTraceEvents(allTraceEvents); - _processing.value = false; + await processAllTraceEvents(); _timelinePollingRateLimiter = RateLimiter( _timelinePollingRateLimit, @@ -243,13 +257,26 @@ class TimelineEventsController extends PerformanceFeatureController } } - FutureOr processAvailableEvents() async { + FutureOr processAllTraceEvents() async { assert(!_processing.value); _processing.value = true; - await processTraceEvents(allTraceEvents); + await _processAllTraceEvents(); _processing.value = false; } + FutureOr _processAllTraceEvents() async { + if (_perfettoMode) { + // TODO(kenz): hook up Perfetto event processor to process events before + // loading. + await perfettoController.loadTrace(allTraceEvents); + } else { + await legacyController.processTraceEvents( + allTraceEvents, + threadNamesById: threadNamesById, + ); + } + } + Future selectTimelineEvent( TimelineEvent? event, { bool updateProfiler = true, @@ -265,19 +292,8 @@ class TimelineEventsController extends PerformanceFeatureController } } - FutureOr processTraceEvents(List traceEvents) async { - if (FeatureFlags.embeddedPerfetto && !useLegacyTraceViewer.value) { - await perfettoController.loadTrace(traceEvents); - } else { - await legacyController.processTraceEvents( - traceEvents, - threadNamesById: threadNamesById, - ); - } - } - @override - void handleSelectedFrame(FlutterFrame frame) async { + Future handleSelectedFrame(FlutterFrame frame) async { if (useLegacyTraceViewer.value) { await _legacySelectFrame(frame); } else if (FeatureFlags.embeddedPerfetto) { @@ -311,7 +327,7 @@ class TimelineEventsController extends PerformanceFeatureController // Only try to pull timeline events for frames that are after the first // well formed frame. Timeline events that occurred before this frame will // have already fallen out of the buffer. - await processAvailableEvents(); + await processAllTraceEvents(); } if (framesController.currentFrameBeingSelected != frame) return; @@ -324,9 +340,9 @@ class TimelineEventsController extends PerformanceFeatureController _processing.value = true; await Future.delayed(_timelinePollingInterval, () async { if (framesController.currentFrameBeingSelected != frame) return; - await processTraceEvents(allTraceEvents); - _processing.value = false; + await _processAllTraceEvents(); }); + _processing.value = false; } if (framesController.currentFrameBeingSelected != frame) return; @@ -421,11 +437,9 @@ class TimelineEventsController extends PerformanceFeatureController _httpTimelineLoggingEnabled.value = state; } - void toggleUseLegacyTraceViewer(bool? value) { + Future toggleUseLegacyTraceViewer(bool? value) async { useLegacyTraceViewer.value = value ?? false; - // `unawaited` does not work for FutureOr - // ignore: discarded_futures - processAvailableEvents(); + await processAllTraceEvents(); } void recordTrace(Map trace) { @@ -469,7 +483,7 @@ class TimelineEventsController extends PerformanceFeatureController ..clear() ..addAll(traceEvents); _primeThreadIds(traceEvents); - await processAvailableEvents(); + await processAllTraceEvents(); await legacyController.setOfflineData(offlineData); } 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 1e3daace383..9c924525b1b 100644 --- a/packages/devtools_app/lib/src/screens/performance/performance_controller.dart +++ b/packages/devtools_app/lib/src/screens/performance/performance_controller.dart @@ -141,11 +141,13 @@ class PerformanceController extends DisposableController await Future.wait(futures); } - // TODO(kenz): use this on tab switches to delay each feature handling a frame - // selection until they need to. - void setActiveFeature(PerformanceFeatureController featureController) { - _applyToFeatureControllers( - (c) => c.isActiveFeature = c == featureController, + Future setActiveFeature( + PerformanceFeatureController? featureController, + ) async { + await _applyToFeatureControllersAsync( + (c) async => await c.setIsActiveFeature( + featureController != null && c == featureController, + ), ); } @@ -195,7 +197,18 @@ abstract class PerformanceFeatureController extends DisposableController { PerformanceData? get data => performanceController.data; - bool isActiveFeature = false; + /// Whether this feature is active and visible to the user. + bool get isActiveFeature => _isActiveFeature; + bool _isActiveFeature = false; + + Future setIsActiveFeature(bool value) async { + _isActiveFeature = value; + if (value) { + await onBecomingActive(); + } + } + + Future onBecomingActive() async {} Future init() async {} 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 9c99fa28536..598abc43c60 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 @@ -102,6 +102,10 @@ class _TabbedPerformanceViewState extends State tabs: tabs, tabViews: tabViews, gaScreen: analytics_constants.performance, + onTabChanged: (int index) { + final featureController = featureControllers[index]; + unawaited(controller.setActiveFeature(featureController)); + }, ); } @@ -259,7 +263,7 @@ class RefreshTimelineEventsButton extends StatelessWidget { Widget build(BuildContext context) { return DevToolsIconButton( iconData: Icons.refresh, - onPressed: controller.processAvailableEvents, + onPressed: controller.processAllTraceEvents, tooltip: 'Refresh timeline events', gaScreen: analytics_constants.performance, gaSelection: analytics_constants.refreshTimelineEvents, diff --git a/packages/devtools_app/lib/src/ui/tab.dart b/packages/devtools_app/lib/src/ui/tab.dart index 21953977666..c28cafe3ee8 100644 --- a/packages/devtools_app/lib/src/ui/tab.dart +++ b/packages/devtools_app/lib/src/ui/tab.dart @@ -81,6 +81,7 @@ class AnalyticsTabbedView extends StatefulWidget { required this.gaScreen, this.outlined = true, this.sendAnalytics = true, + this.onTabChanged, }) : trailingWidgets = List.generate( tabs.length, (index) => tabs[index].trailing ?? const SizedBox(), @@ -103,6 +104,8 @@ class AnalyticsTabbedView extends StatefulWidget { /// experimental code we do not want to send GA events for yet. final bool sendAnalytics; + final void Function(int)? onTabChanged; + @override _AnalyticsTabbedViewState createState() => _AnalyticsTabbedViewState(); } @@ -143,6 +146,7 @@ class _AnalyticsTabbedViewState extends State if (_currentTabControllerIndex != newIndex) { setState(() { _currentTabControllerIndex = newIndex; + widget.onTabChanged?.call(newIndex); }); if (widget.sendAnalytics) { ga.select( diff --git a/packages/devtools_app/test/performance/flutter_frames/flutter_frames_controller_test.dart b/packages/devtools_app/test/performance/flutter_frames/flutter_frames_controller_test.dart index 1ecbab82860..60f70a6f151 100644 --- a/packages/devtools_app/test/performance/flutter_frames/flutter_frames_controller_test.dart +++ b/packages/devtools_app/test/performance/flutter_frames/flutter_frames_controller_test.dart @@ -77,6 +77,7 @@ void main() async { mockTimelineEventsController.handleSelectedFrame(any), ).thenAnswer((_) { timelineControllerHandlerCalled = true; + return Future.value(); }); expect(timelineControllerHandlerCalled, isFalse); diff --git a/packages/devtools_app/test/performance/performance_controller_test.dart b/packages/devtools_app/test/performance/performance_controller_test.dart index 7d1066dfbe3..0409064dcbc 100644 --- a/packages/devtools_app/test/performance/performance_controller_test.dart +++ b/packages/devtools_app/test/performance/performance_controller_test.dart @@ -1,10 +1,50 @@ // Copyright 2019 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_app/src/config_specific/import_export/import_export.dart'; +import 'package:devtools_test/devtools_test.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +// TODO(kenz): add better test coverage for [PerformanceController]. void main() async { - test('performance controller', () { - // TODO(kenz): add better test coverage for [PerformanceController]. + late PerformanceController controller; + late MockServiceConnectionManager mockServiceManager; + + group('$PerformanceController', () { + setUp(() { + setGlobal(IdeTheme, IdeTheme()); + setGlobal(OfflineModeController, OfflineModeController()); + mockServiceManager = MockServiceConnectionManager(); + final connectedApp = MockConnectedApp(); + mockConnectedApp( + connectedApp, + isFlutterApp: true, + isProfileBuild: false, + isWebApp: false, + ); + when(mockServiceManager.connectedApp).thenReturn(connectedApp); + setGlobal(ServiceConnectionManager, mockServiceManager); + offlineController.enterOfflineMode(); + controller = PerformanceController(); + }); + + test('setActiveFeature', () { + expect(controller.flutterFramesController.isActiveFeature, isTrue); + expect(controller.timelineEventsController.isActiveFeature, isFalse); + expect(controller.rasterStatsController.isActiveFeature, isFalse); + + controller.setActiveFeature(controller.timelineEventsController); + expect(controller.flutterFramesController.isActiveFeature, isTrue); + expect(controller.timelineEventsController.isActiveFeature, isTrue); + expect(controller.rasterStatsController.isActiveFeature, isFalse); + + controller.setActiveFeature(controller.rasterStatsController); + expect(controller.flutterFramesController.isActiveFeature, isTrue); + expect(controller.timelineEventsController.isActiveFeature, isFalse); + expect(controller.rasterStatsController.isActiveFeature, isTrue); + }); }); }