Persist setting for flutter frames chart visibility and expose control in offline mode (#5339)
diff --git a/packages/devtools_app/integration_test/test_infra/goldens/perfetto_frame_selection.png b/packages/devtools_app/integration_test/test_infra/goldens/perfetto_frame_selection.png index fb67d73..794a544 100644 --- a/packages/devtools_app/integration_test/test_infra/goldens/perfetto_frame_selection.png +++ b/packages/devtools_app/integration_test/test_infra/goldens/perfetto_frame_selection.png Binary files differ
diff --git a/packages/devtools_app/integration_test/test_infra/goldens/perfetto_frame_selection_2.png b/packages/devtools_app/integration_test/test_infra/goldens/perfetto_frame_selection_2.png index fb67d73..794a544 100644 --- a/packages/devtools_app/integration_test/test_infra/goldens/perfetto_frame_selection_2.png +++ b/packages/devtools_app/integration_test/test_infra/goldens/perfetto_frame_selection_2.png Binary files differ
diff --git a/packages/devtools_app/integration_test/test_infra/goldens/perfetto_initial_load.png b/packages/devtools_app/integration_test/test_infra/goldens/perfetto_initial_load.png index 4ef07f0..475a9c9 100644 --- a/packages/devtools_app/integration_test/test_infra/goldens/perfetto_initial_load.png +++ b/packages/devtools_app/integration_test/test_infra/goldens/perfetto_initial_load.png Binary files differ
diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart index e5ad6f8..af521bb 100644 --- a/packages/devtools_app/lib/src/app.dart +++ b/packages/devtools_app/lib/src/app.dart
@@ -46,11 +46,11 @@ import 'shared/console/primitives/simple_items.dart'; import 'shared/dialogs.dart'; import 'shared/globals.dart'; +import 'shared/offline_screen.dart'; import 'shared/primitives/auto_dispose.dart'; import 'shared/primitives/utils.dart'; import 'shared/routing.dart'; import 'shared/screen.dart'; -import 'shared/snapshot_screen.dart'; import 'shared/theme.dart'; import 'shared/ui/hover.dart'; @@ -277,13 +277,13 @@ for (final screen in widget.screens) screen.screen.screenId: _buildTabbedPage, snapshotPageId: (_, __, args, ___) { - final snapshotArgs = SnapshotArguments.fromArgs(args); + final snapshotArgs = OfflineDataArguments.fromArgs(args); return DevToolsScaffold.withChild( key: UniqueKey(), ideTheme: ideTheme, child: MultiProvider( providers: _providedControllers(offline: true), - child: SnapshotScreenBody(snapshotArgs, _screens), + child: OfflineScreenBody(snapshotArgs, _screens), ), ); },
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/controls/enhance_tracing/enhance_tracing.dart b/packages/devtools_app/lib/src/screens/performance/panes/controls/enhance_tracing/enhance_tracing.dart index 15c1eed..feb20d9 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/controls/enhance_tracing/enhance_tracing.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/controls/enhance_tracing/enhance_tracing.dart
@@ -13,7 +13,7 @@ import '../../../../../shared/globals.dart'; import '../../../../../shared/primitives/auto_dispose.dart'; import '../../../../../shared/theme.dart'; -import '../../../performance_screen.dart'; +import '../performance_controls.dart'; import 'enhance_tracing_controller.dart'; class EnhanceTracingButton extends StatelessWidget { @@ -37,7 +37,7 @@ icon: icon, tooltip: 'Add more detail to the Timeline trace', minScreenWidthForTextBeforeScaling: - SecondaryPerformanceControls.minScreenWidthForTextBeforeScaling, + PerformanceControls.minScreenWidthForTextBeforeScaling, extensions: enhanceTracingExtensions, forceShowOverlayController: enhanceTracingController.showMenuStreamController,
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/controls/more_debugging_options.dart b/packages/devtools_app/lib/src/screens/performance/panes/controls/more_debugging_options.dart index 0b0f85d..eec753b 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/controls/more_debugging_options.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/controls/more_debugging_options.dart
@@ -9,7 +9,7 @@ import '../../../../shared/feature_flags.dart'; import '../../../../shared/globals.dart'; import '../../../../shared/theme.dart'; -import '../../performance_screen.dart'; +import 'performance_controls.dart'; class MoreDebuggingOptionsButton extends StatelessWidget { const MoreDebuggingOptionsButton({Key? key}) : super(key: key); @@ -23,7 +23,7 @@ icon: Icons.build, tooltip: 'Opens a list of options you can use to help debug performance', minScreenWidthForTextBeforeScaling: - SecondaryPerformanceControls.minScreenWidthForTextBeforeScaling, + PerformanceControls.minScreenWidthForTextBeforeScaling, extensions: [ extensions.disableClipLayers, extensions.disableOpacityLayers,
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 new file mode 100644 index 0000000..8e25f66 --- /dev/null +++ b/packages/devtools_app/lib/src/screens/performance/panes/controls/performance_controls.dart
@@ -0,0 +1,167 @@ +// 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 'dart:async'; + +import 'package:flutter/material.dart'; + +import '../../../../service/service_extension_widgets.dart'; +import '../../../../service/service_extensions.dart' as extensions; +import '../../../../shared/analytics/analytics.dart' as ga; +import '../../../../shared/analytics/constants.dart' as gac; +import '../../../../shared/common_widgets.dart'; +import '../../../../shared/globals.dart'; +import '../../../../shared/theme.dart'; +import '../../panes/timeline_events/timeline_events_controller.dart'; +import '../../performance_controller.dart'; +import 'enhance_tracing/enhance_tracing.dart'; +import 'more_debugging_options.dart'; +import 'performance_settings.dart'; + +class PerformanceControls extends StatelessWidget { + const PerformanceControls({ + required this.controller, + required this.onClear, + }); + + static const minScreenWidthForTextBeforeScaling = 920.0; + + final PerformanceController controller; + + final VoidCallback onClear; + + @override + Widget build(BuildContext context) { + return OfflineAwareControls( + controlsBuilder: (offline) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ValueListenableBuilder<EventsControllerStatus>( + valueListenable: controller.timelineEventsController.status, + builder: (context, status, _) { + return _PrimaryControls( + controller: controller, + processing: status == EventsControllerStatus.processing, + offline: offline, + onClear: onClear, + ); + }, + ), + if (!offline) + Padding( + padding: const EdgeInsets.only(left: defaultSpacing), + child: _SecondaryPerformanceControls(controller: controller), + ), + ], + ); + }, + ); + } +} + +class _PrimaryControls extends StatelessWidget { + const _PrimaryControls({ + Key? key, + required this.controller, + required this.processing, + required this.offline, + required this.onClear, + }) : super(key: key); + + final PerformanceController controller; + + final bool processing; + + final bool offline; + + final VoidCallback onClear; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + if (serviceManager.connectedApp!.isFlutterAppNow!) ...[ + VisibilityButton( + show: preferences.performance.showFlutterFramesChart, + onPressed: + controller.flutterFramesController.toggleShowFlutterFrames, + label: 'Flutter frames', + tooltip: 'Toggle visibility of the Flutter frames chart', + ), + const SizedBox(width: denseSpacing), + ], + if (!offline) + OutlinedIconButton( + icon: Icons.block, + tooltip: 'Clear all data on the Performance screen', + onPressed: processing ? null : _clearPerformanceData, + ), + ], + ); + } + + Future<void> _clearPerformanceData() async { + ga.select(gac.performance, gac.clear); + await controller.clearData(); + onClear(); + } +} + +class _SecondaryPerformanceControls extends StatelessWidget { + const _SecondaryPerformanceControls({ + Key? key, + required this.controller, + }) : super(key: key); + + final PerformanceController controller; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (serviceManager.connectedApp!.isFlutterAppNow!) ...[ + ServiceExtensionButtonGroup( + minScreenWidthForTextBeforeScaling: + PerformanceControls.minScreenWidthForTextBeforeScaling, + extensions: [ + extensions.performanceOverlay, + ], + ), + const SizedBox(width: denseSpacing), + EnhanceTracingButton(controller.enhanceTracingController), + const SizedBox(width: denseSpacing), + const MoreDebuggingOptionsButton(), + ], + const SizedBox(width: defaultSpacing), + OutlinedIconButton( + icon: Icons.file_download, + tooltip: 'Export data', + onPressed: _exportPerformanceData, + ), + const SizedBox(width: denseSpacing), + SettingsOutlinedButton( + onPressed: () => _openSettingsDialog(context), + ), + ], + ); + } + + void _exportPerformanceData() { + ga.select(gac.performance, gac.export); + controller.exportData(); + // TODO(kenz): investigate if we need to do any error handling here. Is the + // download always successful? + } + + void _openSettingsDialog(BuildContext context) { + unawaited( + showDialog( + context: context, + builder: (context) => PerformanceSettingsDialog(controller), + ), + ); + } +}
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_chart.dart b/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_chart.dart index 2ccef1b..943249a 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_chart.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/flutter_frames/flutter_frames_chart.dart
@@ -49,7 +49,7 @@ secondListenable: framesController.displayRefreshRate, builder: (context, frames, displayRefreshRate, child) { return ValueListenableBuilder<bool>( - valueListenable: framesController.showFlutterFramesChart, + valueListenable: preferences.performance.showFlutterFramesChart, builder: (context, show, _) { return _FlutterFramesChart( framesController: framesController,
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 c9c16ad..f9ff023 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
@@ -28,12 +28,10 @@ ValueListenable<List<FlutterFrame>> get flutterFrames => _flutterFrames; final _flutterFrames = ListValueNotifier<FlutterFrame>([]); - /// Whether we should show the Flutter frames chart. - ValueListenable<bool> get showFlutterFramesChart => _showFlutterFramesChart; - final _showFlutterFramesChart = ValueNotifier<bool>(true); + /// Controls the visibility of the Flutter frames chart. void toggleShowFlutterFrames(bool value) { - _showFlutterFramesChart.value = value; - unawaited(setIsActiveFeature(_showFlutterFramesChart.value)); + preferences.performance.showFlutterFramesChart.value = value; + unawaited(setIsActiveFeature(value)); } /// Whether flutter frames are currently being recorded. @@ -93,7 +91,9 @@ @override Future<void> setIsActiveFeature(bool value) async { final isFlutterApp = serviceManager.connectedApp?.isFlutterAppNow ?? false; - value = isFlutterApp && _showFlutterFramesChart.value; + final shouldShowFramesChart = + preferences.performance.showFlutterFramesChart.value; + value = isFlutterApp && shouldShowFramesChart; await super.setIsActiveFeature(value); }
diff --git a/packages/devtools_app/lib/src/screens/performance/performance_screen.dart b/packages/devtools_app/lib/src/screens/performance/performance_screen.dart index c76ece8..be3a149 100644 --- a/packages/devtools_app/lib/src/screens/performance/performance_screen.dart +++ b/packages/devtools_app/lib/src/screens/performance/performance_screen.dart
@@ -2,15 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:devtools_shared/devtools_shared.dart'; import 'package:flutter/material.dart'; -import '../../service/service_extension_widgets.dart'; -import '../../service/service_extensions.dart' as extensions; import '../../shared/analytics/analytics.dart' as ga; -import '../../shared/analytics/constants.dart' as gac; import '../../shared/banner_messages.dart'; import '../../shared/common_widgets.dart'; import '../../shared/globals.dart'; @@ -20,11 +15,8 @@ import '../../shared/theme.dart'; import '../../shared/ui/icons.dart'; import '../../shared/utils.dart'; -import 'panes/controls/enhance_tracing/enhance_tracing.dart'; -import 'panes/controls/more_debugging_options.dart'; -import 'panes/controls/performance_settings.dart'; +import 'panes/controls/performance_controls.dart'; import 'panes/flutter_frames/flutter_frames_chart.dart'; -import 'panes/timeline_events/timeline_events_controller.dart'; import 'performance_controller.dart'; import 'tabbed_performance_view.dart'; @@ -112,7 +104,10 @@ controller.offlinePerformanceData!.frames.isNotEmpty; return Column( children: [ - if (!offlineMode) _buildPerformanceControls(), + PerformanceControls( + controller: controller, + onClear: () => setState(() {}), + ), const SizedBox(height: denseRowSpacing), if (isOfflineFlutterApp || (!offlineMode && serviceManager.connectedApp!.isFlutterAppNow!)) @@ -126,127 +121,4 @@ }, ); } - - Widget _buildPerformanceControls() { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ValueListenableBuilder<EventsControllerStatus>( - valueListenable: controller.timelineEventsController.status, - builder: (context, status, _) { - return _PrimaryControls( - controller: controller, - processing: status == EventsControllerStatus.processing, - onClear: () => setState(() {}), - ); - }, - ), - const SizedBox(width: defaultSpacing), - SecondaryPerformanceControls(controller: controller), - ], - ); - } -} - -class _PrimaryControls extends StatelessWidget { - const _PrimaryControls({ - Key? key, - required this.controller, - required this.processing, - required this.onClear, - }) : super(key: key); - - final PerformanceController controller; - - final bool processing; - - final VoidCallback onClear; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - if (serviceManager.connectedApp!.isFlutterAppNow!) ...[ - VisibilityButton( - show: controller.flutterFramesController.showFlutterFramesChart, - onPressed: - controller.flutterFramesController.toggleShowFlutterFrames, - label: 'Flutter frames', - tooltip: 'Toggle visibility of the Flutter frames chart', - ), - const SizedBox(width: denseSpacing), - ], - OutlinedIconButton( - icon: Icons.block, - tooltip: 'Clear all data on the Performance screen', - onPressed: processing ? null : _clearPerformanceData, - ), - ], - ); - } - - Future<void> _clearPerformanceData() async { - ga.select(gac.performance, gac.clear); - await controller.clearData(); - onClear(); - } -} - -class SecondaryPerformanceControls extends StatelessWidget { - const SecondaryPerformanceControls({ - Key? key, - required this.controller, - }) : super(key: key); - - static const minScreenWidthForTextBeforeScaling = 920.0; - - final PerformanceController controller; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (serviceManager.connectedApp!.isFlutterAppNow!) ...[ - ServiceExtensionButtonGroup( - minScreenWidthForTextBeforeScaling: - minScreenWidthForTextBeforeScaling, - extensions: [ - extensions.performanceOverlay, - ], - ), - const SizedBox(width: denseSpacing), - EnhanceTracingButton(controller.enhanceTracingController), - const SizedBox(width: denseSpacing), - const MoreDebuggingOptionsButton(), - ], - const SizedBox(width: defaultSpacing), - OutlinedIconButton( - icon: Icons.file_download, - tooltip: 'Export data', - onPressed: _exportPerformanceData, - ), - const SizedBox(width: denseSpacing), - SettingsOutlinedButton( - onPressed: () => _openSettingsDialog(context), - ), - ], - ); - } - - void _exportPerformanceData() { - ga.select(gac.performance, gac.export); - controller.exportData(); - // TODO(kenz): investigate if we need to do any error handling here. Is the - // download always successful? - } - - void _openSettingsDialog(BuildContext context) { - unawaited( - showDialog( - context: context, - builder: (context) => PerformanceSettingsDialog(controller), - ), - ); - } }
diff --git a/packages/devtools_app/lib/src/screens/profiler/profiler_screen.dart b/packages/devtools_app/lib/src/screens/profiler/profiler_screen.dart index a109fc9..044e5d9 100644 --- a/packages/devtools_app/lib/src/screens/profiler/profiler_screen.dart +++ b/packages/devtools_app/lib/src/screens/profiler/profiler_screen.dart
@@ -171,12 +171,12 @@ ); final profilerScreen = Column( children: [ - if (!offlineController.offlineMode.value) - ProfilerScreenControls( - controller: controller, - recording: recording, - processing: processing, - ), + ProfilerScreenControls( + controller: controller, + recording: recording, + processing: processing, + offline: offlineController.offlineMode.value, + ), const SizedBox(height: denseRowSpacing), Expanded( child: OutlineDecoration( @@ -261,6 +261,7 @@ required this.controller, required this.recording, required this.processing, + required this.offline, }); final ProfilerScreenController controller; @@ -269,20 +270,29 @@ final bool processing; + final bool offline; + @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _PrimaryControls( - controller: controller, - recording: recording, - ), - const SizedBox(width: defaultSpacing), - _SecondaryControls( - controller: controller, - profilerBusy: recording || processing, - ), + if (offline) + const Padding( + padding: EdgeInsets.only(right: defaultSpacing), + child: ExitOfflineButton(), + ) + else ...[ + _PrimaryControls( + controller: controller, + recording: recording, + ), + const SizedBox(width: defaultSpacing), + _SecondaryControls( + controller: controller, + profilerBusy: recording || processing, + ), + ], ], ); }
diff --git a/packages/devtools_app/lib/src/shared/analytics/constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants.dart index 178e332..53d4ac9 100644 --- a/packages/devtools_app/lib/src/shared/analytics/constants.dart +++ b/packages/devtools_app/lib/src/shared/analytics/constants.dart
@@ -59,6 +59,7 @@ const performanceOverlay = 'performanceOverlay'; const performanceOverlayDocs = 'performanceOverlayDocs'; const timelineFlameChartHelp = 'timelineFlameChartHelp'; +const framesChartVisibility = 'framesChartVisibility'; const selectFlutterFrame = 'selectFlutterFrame'; const traceEventProcessingTime = 'traceEventProcessingTime'; const trackRebuilds = 'trackRebuilds'; @@ -79,7 +80,6 @@ const intrinsicOperationsDocs = 'intrinsicOperationsDocs'; const shaderCompilationDocs = 'shaderCompilationDocs'; const shaderCompilationDocsTooltipLink = 'shaderCompilationDocsTooltipLink'; -const analyzeSelectedFrame = 'analyzeSelectedFrame'; const collectRasterStats = 'collectRasterStats'; const perfettoModeTraceEventProcessingTime = 'traceEventProcessingTime-perfettoMode';
diff --git a/packages/devtools_app/lib/src/shared/common_widgets.dart b/packages/devtools_app/lib/src/shared/common_widgets.dart index 92d4f14..395f012 100644 --- a/packages/devtools_app/lib/src/shared/common_widgets.dart +++ b/packages/devtools_app/lib/src/shared/common_widgets.dart
@@ -23,6 +23,7 @@ import 'primitives/auto_dispose.dart'; import 'primitives/flutter_widgets/linked_scroll_controller.dart'; import 'primitives/utils.dart'; +import 'routing.dart'; import 'theme.dart'; import 'ui/icons.dart'; import 'ui/label.dart'; @@ -717,21 +718,57 @@ } /// Common button for exiting offline mode. -/// -/// Consumers of this widget will be responsible for including the following in -/// onPressed: -/// -/// setState(() { -/// offlineController.exitOfflineMode(); -/// } -class ExitOfflineButton extends IconLabelButton { - const ExitOfflineButton({required VoidCallback onPressed}) - : super( - key: const Key('exit offline button'), - onPressed: onPressed, - label: 'Exit offline mode', - icon: Icons.clear, +class ExitOfflineButton extends StatelessWidget { + const ExitOfflineButton({super.key}); + + @override + Widget build(BuildContext context) { + final routerDelegate = DevToolsRouterDelegate.of(context); + return IconLabelButton( + key: const Key('exit offline button'), + label: 'Exit offline mode', + icon: Icons.clear, + onPressed: () { + offlineController.exitOfflineMode(); + // Use Router.neglect to replace the current history entry with + // the homepage so that clicking Back will not return here. + Router.neglect( + context, + () => routerDelegate.navigateHome(clearScreenParam: true), ); + }, + ); + } +} + +class OfflineAwareControls extends StatelessWidget { + const OfflineAwareControls({ + required this.controlsBuilder, + super.key, + }); + + final Widget Function(bool) controlsBuilder; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder<bool>( + valueListenable: offlineController.offlineMode, + builder: (context, offline, _) { + return Row( + children: [ + if (offlineController.offlineMode.value) + const Padding( + padding: EdgeInsets.only(right: defaultSpacing), + child: ExitOfflineButton(), + ), + Expanded( + child: controlsBuilder(offline), + ), + ], + ); + }, + ); + } } /// A small element containing some accessory information, often a numeric
diff --git a/packages/devtools_app/lib/src/shared/config_specific/import_export/import_export.dart b/packages/devtools_app/lib/src/shared/config_specific/import_export/import_export.dart index 808d614..0f11b06 100644 --- a/packages/devtools_app/lib/src/shared/config_specific/import_export/import_export.dart +++ b/packages/devtools_app/lib/src/shared/config_specific/import_export/import_export.dart
@@ -202,5 +202,6 @@ void exitOfflineMode() { serviceManager.connectedApp = _previousConnectedApp; _offlineMode.value = false; + offlineDataJson.clear(); } }
diff --git a/packages/devtools_app/lib/src/shared/offline_screen.dart b/packages/devtools_app/lib/src/shared/offline_screen.dart new file mode 100644 index 0000000..eef0314 --- /dev/null +++ b/packages/devtools_app/lib/src/shared/offline_screen.dart
@@ -0,0 +1,106 @@ +// 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:collection/collection.dart' show IterableExtension; +import 'package:flutter/material.dart'; + +import 'common_widgets.dart'; +import 'globals.dart'; +import 'primitives/auto_dispose.dart'; +import 'screen.dart'; +import 'theme.dart'; + +/// The screen used for displaying offline data (data that does not require a +/// connected app to explore). +/// +/// This screen can be displayed both when DevTools is and is not connected to +/// an app. +class OfflineScreenBody extends StatefulWidget { + const OfflineScreenBody(this.args, this.possibleScreens); + + final OfflineDataArguments args; + + /// All possible screens, both visible and hidden, that DevTools was started + /// with. + /// + /// This will include screens that are only available when connected to an app + /// as well as screens that are only available based on the presence of a + /// conditional library. + /// + /// These screens are stored here so that we can import files for all screens, + /// regardless of whether an app is connected to DevTools or whether a + /// connected app contains the correct conditional library. + final List<Screen> possibleScreens; + + @override + _OfflineScreenBodyState createState() => _OfflineScreenBodyState(); +} + +class _OfflineScreenBodyState extends State<OfflineScreenBody> + with AutoDisposeMixin { + Screen? _screen; + + @override + void initState() { + super.initState(); + _initScreen(); + addAutoDisposeListener(offlineController.offlineMode, () { + if (!offlineController.offlineMode.value) { + setState(() { + _screen = null; + }); + } + }); + } + + @override + void didUpdateWidget(OfflineScreenBody oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.args != oldWidget.args || + widget.possibleScreens != oldWidget.possibleScreens) { + _initScreen(); + } + } + + void _initScreen() { + _screen = widget.possibleScreens.firstWhereOrNull( + (s) => s.screenId == widget.args.screenId, + ); + } + + @override + Widget build(BuildContext context) { + return _screen != null + ? _screen!.build(context) + : _OfflineLoadError(screenId: widget.args.screenId); + } +} + +class _OfflineLoadError extends StatelessWidget { + const _OfflineLoadError({required this.screenId}); + + final String? screenId; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + CenteredMessage( + 'Cannot load offline data for screen \'$screenId\'', + ), + const SizedBox(height: defaultSpacing), + const ExitOfflineButton(), + ], + ); + } +} + +class OfflineDataArguments { + OfflineDataArguments(this.screenId); + + OfflineDataArguments.fromArgs(Map<String, String?> args) + : this(args['screen']); + + final String? screenId; +}
diff --git a/packages/devtools_app/lib/src/shared/preferences.dart b/packages/devtools_app/lib/src/shared/preferences.dart index 6a28366..ead7ca7 100644 --- a/packages/devtools_app/lib/src/shared/preferences.dart +++ b/packages/devtools_app/lib/src/shared/preferences.dart
@@ -34,6 +34,9 @@ MemoryPreferencesController get memory => _memory; final _memory = MemoryPreferencesController(); + PerformancePreferencesController get performance => _performance; + final _performance = PerformancePreferencesController(); + CpuProfilerPreferencesController get cpuProfiler => _cpuProfiler; final _cpuProfiler = CpuProfilerPreferencesController(); @@ -62,6 +65,7 @@ await inspector.init(); await memory.init(); + await performance.init(); await cpuProfiler.init(); setGlobal(PreferencesController, this); @@ -71,6 +75,8 @@ void dispose() { inspector.dispose(); memory.dispose(); + performance.dispose(); + cpuProfiler.dispose(); super.dispose(); } @@ -412,3 +418,30 @@ await storage.getValue(_displayTreeGuidelinesId) == 'true'; } } + +class PerformancePreferencesController extends DisposableController + with AutoDisposeControllerMixin { + final showFlutterFramesChart = ValueNotifier<bool>(true); + + static final _showFlutterFramesChartId = + '${gac.performance}.${gac.framesChartVisibility}'; + + Future<void> init() async { + addAutoDisposeListener( + showFlutterFramesChart, + () { + storage.setValue( + _showFlutterFramesChartId, + showFlutterFramesChart.value.toString(), + ); + ga.select( + gac.performance, + gac.framesChartVisibility, + value: showFlutterFramesChart.value ? 1 : 0, + ); + }, + ); + showFlutterFramesChart.value = + await storage.getValue(_showFlutterFramesChartId) != 'false'; + } +}
diff --git a/packages/devtools_app/lib/src/shared/snapshot_screen.dart b/packages/devtools_app/lib/src/shared/snapshot_screen.dart deleted file mode 100644 index 5758203..0000000 --- a/packages/devtools_app/lib/src/shared/snapshot_screen.dart +++ /dev/null
@@ -1,118 +0,0 @@ -// 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:collection/collection.dart' show IterableExtension; -import 'package:flutter/material.dart'; - -import 'common_widgets.dart'; -import 'globals.dart'; -import 'routing.dart'; -import 'screen.dart'; -import 'theme.dart'; - -/// The screen used for displaying a snapshot of imported data. -/// -/// This screen can be displayed both when DevTools is and is not connected to -/// an app. -class SnapshotScreenBody extends StatefulWidget { - const SnapshotScreenBody(this.args, this.possibleScreens); - - final SnapshotArguments args; - - /// All possible screens, both visible and hidden, that DevTools was started - /// with. - /// - /// This will include screens that are only available when connected to an app - /// as well as screens that are only available based on the presence of a - /// conditional library. - /// - /// These screens are stored here so that we can import files for all screens, - /// regardless of whether an app is connected to DevTools or whether a - /// connected app contains the correct conditional library. - final List<Screen> possibleScreens; - - @override - _SnapshotScreenBodyState createState() => _SnapshotScreenBodyState(); -} - -class _SnapshotScreenBodyState extends State<SnapshotScreenBody> { - Screen? _screen; - - @override - void initState() { - super.initState(); - _initScreen(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - } - - @override - void didUpdateWidget(SnapshotScreenBody oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.args != oldWidget.args || - widget.possibleScreens != oldWidget.possibleScreens) { - _initScreen(); - } - } - - void _initScreen() { - _screen = widget.possibleScreens.firstWhereOrNull( - (s) => s.screenId == widget.args.screenId, - ); - } - - @override - Widget build(BuildContext context) { - final routerDelegate = DevToolsRouterDelegate.of(context); - return Column( - children: [ - Row( - children: [ - ExitOfflineButton( - onPressed: () { - offlineController.exitOfflineMode(); - reset(); - // Use Router.neglect to replace the current history entry with - // the homepage so that clicking Back will not return here. - Router.neglect( - context, - () => routerDelegate.navigateHome(clearScreenParam: true), - ); - }, - ), - ], - ), - const SizedBox(height: denseRowSpacing), - Expanded( - child: - _screen != null ? _screen!.build(context) : _buildSnapshotError(), - ), - ], - ); - } - - Widget _buildSnapshotError() { - return CenteredMessage( - 'Cannot load snapshot for screen \'${widget.args.screenId}\'', - ); - } - - void reset() { - setState(() { - offlineController.offlineDataJson.clear(); - _screen = null; - }); - } -} - -class SnapshotArguments { - SnapshotArguments(this.screenId); - - SnapshotArguments.fromArgs(Map<String, String?> args) : this(args['screen']); - - final String? screenId; -}
diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 2df6916..c6ba6a4 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
@@ -12,7 +12,7 @@ TODO: Remove this section if there are not any general updates. ## Performance updates -TODO: Remove this section if there are not any general updates. +* Persist a user's preference for whether the Flutter Frames chart should be shown by default. - [#5339](https://github.com/flutter/devtools/pull/5339) ## CPU profiler updates * Add ability to inspect statistics for a CPU profile - [#5340](https://github.com/flutter/devtools/pull/5340)
diff --git a/packages/devtools_app/test/performance/enhance_tracing_test.dart b/packages/devtools_app/test/performance/controls/enhance_tracing_test.dart similarity index 100% rename from packages/devtools_app/test/performance/enhance_tracing_test.dart rename to packages/devtools_app/test/performance/controls/enhance_tracing_test.dart
diff --git a/packages/devtools_app/test/performance/controls/performance_controls_test.dart b/packages/devtools_app/test/performance/controls/performance_controls_test.dart new file mode 100644 index 0000000..421ee2b --- /dev/null +++ b/packages/devtools_app/test/performance/controls/performance_controls_test.dart
@@ -0,0 +1,109 @@ +// 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/screens/performance/panes/controls/performance_controls.dart'; +import 'package:devtools_app/src/shared/config_specific/import_export/import_export.dart'; +import 'package:devtools_test/devtools_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +void main() { + const windowSize = Size(3000.0, 1000.0); + + setUp(() { + setGlobal(DevToolsExtensionPoints, ExternalDevToolsExtensionPoints()); + setGlobal(IdeTheme, IdeTheme()); + setGlobal(PreferencesController, PreferencesController()); + setGlobal(OfflineModeController, OfflineModeController()); + setGlobal(NotificationService, NotificationService()); + }); + + group('$PerformanceControls', () { + late MockServiceConnectionManager mockServiceManager; + late MockPerformanceController mockPerformanceController; + + setUp(() { + mockServiceManager = MockServiceConnectionManager(); + when(mockServiceManager.serviceExtensionManager) + .thenReturn(FakeServiceExtensionManager()); + final connectedApp = MockConnectedApp(); + mockConnectedApp( + connectedApp, + isFlutterApp: true, + isProfileBuild: false, + isWebApp: false, + ); + when(mockServiceManager.connectedApp).thenReturn(connectedApp); + setGlobal(ServiceConnectionManager, mockServiceManager); + mockPerformanceController = createMockPerformanceControllerWithDefaults(); + }); + + tearDown(() { + offlineController.exitOfflineMode(); + }); + + Future<void> _pumpControls(WidgetTester tester) async { + await tester.pumpWidget( + wrapWithControllers( + PerformanceControls( + controller: mockPerformanceController, + onClear: () {}, + ), + performance: mockPerformanceController, + ), + ); + await tester.pumpAndSettle(); + } + + testWidgetsWithWindowSize('builds for Flutter app', windowSize, + (WidgetTester tester) async { + await _pumpControls(tester); + expect(find.byType(ExitOfflineButton), findsNothing); + expect(find.byType(VisibilityButton), findsOneWidget); + expect(find.byIcon(Icons.block), findsOneWidget); + expect(find.text('Performance Overlay'), findsOneWidget); + expect(find.text('Enhance Tracing'), findsOneWidget); + expect(find.text('More debugging options'), findsOneWidget); + expect(find.byIcon(Icons.file_download), findsOneWidget); + expect(find.byIcon(Icons.settings), findsOneWidget); + }); + + testWidgetsWithWindowSize('builds for non flutter app', windowSize, + (WidgetTester tester) async { + mockConnectedApp( + mockServiceManager.connectedApp!, + isFlutterApp: false, + isProfileBuild: false, + isWebApp: false, + ); + await _pumpControls(tester); + + expect(find.byType(ExitOfflineButton), findsNothing); + expect(find.byType(VisibilityButton), findsNothing); + expect(find.byIcon(Icons.block), findsOneWidget); + expect(find.text('Performance Overlay'), findsNothing); + expect(find.text('Enhance Tracing'), findsNothing); + expect(find.text('More debugging options'), findsNothing); + expect(find.byIcon(Icons.file_download), findsOneWidget); + expect(find.byIcon(Icons.settings), findsOneWidget); + }); + + testWidgetsWithWindowSize('builds for offline mode', windowSize, + (WidgetTester tester) async { + offlineController.enterOfflineMode(); + await _pumpControls(tester); + expect(find.byType(ExitOfflineButton), findsOneWidget); + expect(find.byType(VisibilityButton), findsOneWidget); + expect(find.byIcon(Icons.block), findsNothing); + expect(find.text('Performance Overlay'), findsNothing); + expect(find.text('Enhance Tracing'), findsNothing); + expect(find.text('More debugging options'), findsNothing); + expect(find.byIcon(Icons.file_download), findsNothing); + expect(find.byIcon(Icons.settings), findsNothing); + offlineController.exitOfflineMode(); + }); + }); +}
diff --git a/packages/devtools_app/test/performance/flutter_frames/flutter_frames_chart_test.dart b/packages/devtools_app/test/performance/flutter_frames/flutter_frames_chart_test.dart index ee9e301..743303f 100644 --- a/packages/devtools_app/test/performance/flutter_frames/flutter_frames_chart_test.dart +++ b/packages/devtools_app/test/performance/flutter_frames/flutter_frames_chart_test.dart
@@ -43,6 +43,8 @@ setGlobal(OfflineModeController, OfflineModeController()); setGlobal(IdeTheme, IdeTheme()); setGlobal(NotificationService, NotificationService()); + setGlobal(DevToolsExtensionPoints, ExternalDevToolsExtensionPoints()); + setGlobal(PreferencesController, PreferencesController()); framesController = FlutterFramesController( createMockPerformanceControllerWithDefaults(),
diff --git a/packages/devtools_app/test/performance/performance_controller_test.dart b/packages/devtools_app/test/performance/performance_controller_test.dart index 32726cc..e187cc2 100644 --- a/packages/devtools_app/test/performance/performance_controller_test.dart +++ b/packages/devtools_app/test/performance/performance_controller_test.dart
@@ -17,6 +17,8 @@ setUp(() { setGlobal(IdeTheme, IdeTheme()); setGlobal(OfflineModeController, OfflineModeController()); + setGlobal(DevToolsExtensionPoints, ExternalDevToolsExtensionPoints()); + setGlobal(PreferencesController, PreferencesController()); mockServiceManager = MockServiceConnectionManager(); final connectedApp = MockConnectedApp(); mockConnectedApp(
diff --git a/packages/devtools_app/test/performance/performance_screen_test.dart b/packages/devtools_app/test/performance/performance_screen_test.dart index 0aea004..c7d117e 100644 --- a/packages/devtools_app/test/performance/performance_screen_test.dart +++ b/packages/devtools_app/test/performance/performance_screen_test.dart
@@ -6,10 +6,13 @@ import 'dart:async'; import 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_app/src/screens/performance/panes/controls/performance_controls.dart'; import 'package:devtools_app/src/screens/performance/panes/flutter_frames/flutter_frames_chart.dart'; import 'package:devtools_app/src/screens/performance/panes/timeline_events/legacy/event_details.dart'; import 'package:devtools_app/src/screens/performance/panes/timeline_events/legacy/timeline_flame_chart.dart'; +import 'package:devtools_app/src/screens/performance/tabbed_performance_view.dart'; import 'package:devtools_app/src/shared/config_specific/import_export/import_export.dart'; +import 'package:devtools_shared/devtools_test_utils.dart'; import 'package:devtools_test/devtools_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -19,66 +22,71 @@ import '../test_infra/test_data/performance.dart'; void main() { - setGlobal(DevToolsExtensionPoints, ExternalDevToolsExtensionPoints()); - setGlobal(IdeTheme, IdeTheme()); - setGlobal(PreferencesController, PreferencesController()); - late PerformanceController controller; - late FakeServiceManager fakeServiceManager; - - Future<void> _setUpServiceManagerWithTimeline( - Map<String, dynamic> timelineJson, - ) async { - fakeServiceManager = FakeServiceManager( - service: FakeServiceManager.createFakeService( - timelineData: vm_service.Timeline.parse(timelineJson), - ), - ); - when( - fakeServiceManager.errorBadgeManager.errorCountNotifier('performance'), - ).thenReturn(ValueNotifier<int>(0)); - final app = fakeServiceManager.connectedApp!; - when(app.initialized).thenReturn(Completer()..complete(true)); - when(app.isDartWebAppNow).thenReturn(false); - when(app.isFlutterAppNow).thenReturn(true); - when(app.isProfileBuild).thenAnswer((_) => Future.value(false)); - when(app.flutterVersionNow).thenReturn( - FlutterVersion.parse((await fakeServiceManager.flutterVersion).json!), - ); - when(app.isDartCliAppNow).thenReturn(false); - when(app.isProfileBuildNow).thenReturn(true); - when(app.isDartWebApp).thenAnswer((_) async => false); - when(app.isProfileBuild).thenAnswer((_) async => false); - setGlobal(ServiceConnectionManager, fakeServiceManager); - setGlobal(NotificationService, NotificationService()); - setGlobal(OfflineModeController, OfflineModeController()); - } - - Future<void> pumpPerformanceScreen( - WidgetTester tester, { - bool runAsync = false, - }) async { - await tester.pumpWidget( - wrapWithControllers( - const PerformanceScreenBody(), - performance: controller, - ), - ); - await tester.pumpAndSettle(); - - if (runAsync) { - // Await a small delay to allow the PerformanceController to complete - // initialization. - await Future.delayed(const Duration(seconds: 1)); - } - - expect(find.byType(PerformanceScreenBody), findsOneWidget); - } - const windowSize = Size(3000.0, 1000.0); - group('PerformanceScreen', () { + setUp(() { + setGlobal(DevToolsExtensionPoints, ExternalDevToolsExtensionPoints()); + setGlobal(IdeTheme, IdeTheme()); + setGlobal(PreferencesController, PreferencesController()); + setGlobal(OfflineModeController, OfflineModeController()); + setGlobal(NotificationService, NotificationService()); + }); + + group('$PerformanceScreen', () { + late PerformanceController controller; + late FakeServiceManager fakeServiceManager; + + Future<void> _setUpServiceManagerWithTimeline( + Map<String, dynamic> timelineJson, + ) async { + fakeServiceManager = FakeServiceManager( + service: FakeServiceManager.createFakeService( + timelineData: vm_service.Timeline.parse(timelineJson), + ), + ); + when( + fakeServiceManager.errorBadgeManager.errorCountNotifier('performance'), + ).thenReturn(ValueNotifier<int>(0)); + final app = fakeServiceManager.connectedApp!; + when(app.initialized).thenReturn(Completer()..complete(true)); + when(app.isDartWebAppNow).thenReturn(false); + when(app.isFlutterAppNow).thenReturn(true); + when(app.isProfileBuild).thenAnswer((_) => Future.value(false)); + when(app.flutterVersionNow).thenReturn( + FlutterVersion.parse((await fakeServiceManager.flutterVersion).json!), + ); + when(app.isDartCliAppNow).thenReturn(false); + when(app.isProfileBuildNow).thenReturn(true); + when(app.isDartWebApp).thenAnswer((_) async => false); + when(app.isProfileBuild).thenAnswer((_) async => false); + setGlobal(ServiceConnectionManager, fakeServiceManager); + } + + Future<void> pumpPerformanceScreen( + WidgetTester tester, { + bool runAsync = false, + }) async { + await tester.pumpWidget( + wrapWithControllers( + const PerformanceScreenBody(), + performance: controller, + ), + ); + await tester.pumpAndSettle(); + + if (runAsync) { + // Await a small delay to allow the PerformanceController to complete + // initialization. + await Future.delayed(const Duration(seconds: 1)); + } + + expect(find.byType(PerformanceScreenBody), findsOneWidget); + } + setUp(() async { + preferences.performance.showFlutterFramesChart.value = true; await _setUpServiceManagerWithTimeline(testTimelineJson); + await shortDelay(); controller = PerformanceController(); await controller.initialized; }); @@ -99,18 +107,13 @@ await tester.runAsync(() async { await pumpPerformanceScreen(tester, runAsync: true); await tester.pumpAndSettle(); + expect(find.byType(PerformanceControls), findsOneWidget); expect(find.byType(FlutterFramesChart), findsOneWidget); + expect(find.byType(TabbedPerformanceView), findsOneWidget); expect( find.text('Select a frame above to view analysis data.'), findsOneWidget, ); - expect(find.byType(VisibilityButton), findsOneWidget); - expect(find.byIcon(Icons.block), findsOneWidget); - expect(find.text('Performance Overlay'), findsOneWidget); - expect(find.text('Enhance Tracing'), findsOneWidget); - expect(find.text('More debugging options'), findsOneWidget); - expect(find.byIcon(Icons.file_download), findsOneWidget); - expect(find.byIcon(Icons.settings), findsOneWidget); }); }); @@ -126,6 +129,7 @@ ); await pumpPerformanceScreen(tester, runAsync: true); await tester.pumpAndSettle(); + expect(find.byType(PerformanceControls), findsOneWidget); expect(find.byType(FlutterFramesChart), findsNothing); expect(find.byType(TimelineFlameChart), findsOneWidget); expect( @@ -133,13 +137,6 @@ findsNothing, ); expect(find.byType(EventDetails), findsOneWidget); - expect(find.byType(VisibilityButton), findsNothing); - expect(find.byIcon(Icons.block), findsOneWidget); - expect(find.text('Performance Overlay'), findsNothing); - expect(find.text('Enhance Tracing'), findsNothing); - expect(find.text('More debugging options'), findsNothing); - expect(find.byIcon(Icons.file_download), findsOneWidget); - expect(find.byIcon(Icons.settings), findsOneWidget); // Verify the state of the splitter. final splitFinder = find.byType(Split); @@ -149,7 +146,7 @@ }); }); - group('Performance controls', () { + group('controls', () { testWidgetsWithWindowSize( 'can expand and collapse flutter frames chart', windowSize, (WidgetTester tester) async { @@ -163,7 +160,7 @@ // The flutter frames chart is visible. expect(find.byType(FramesChartControls), findsOneWidget); expect( - controller.flutterFramesController.showFlutterFramesChart.value, + preferences.performance.showFlutterFramesChart.value, isTrue, ); @@ -173,7 +170,7 @@ // The flutter frames chart should no longer be visible. expect(find.byType(FramesChartControls), findsNothing); expect( - controller.flutterFramesController.showFlutterFramesChart.value, + preferences.performance.showFlutterFramesChart.value, isFalse, ); @@ -183,7 +180,7 @@ // The flutter frames chart should be visible again. expect(find.byType(FramesChartControls), findsOneWidget); expect( - controller.flutterFramesController.showFlutterFramesChart.value, + preferences.performance.showFlutterFramesChart.value, isTrue, ); });
diff --git a/packages/devtools_app/test/shared/preferences_controller_test.dart b/packages/devtools_app/test/shared/preferences_controller_test.dart index b9da825..a7f0e6c 100644 --- a/packages/devtools_app/test/shared/preferences_controller_test.dart +++ b/packages/devtools_app/test/shared/preferences_controller_test.dart
@@ -221,4 +221,55 @@ ); }); }); + + group('$PerformancePreferencesController', () { + late PerformancePreferencesController controller; + late FlutterTestStorage storage; + + setUp(() async { + setGlobal(Storage, storage = FlutterTestStorage()); + controller = PerformancePreferencesController(); + await controller.init(); + }); + + test('has expected default values', () async { + expect(controller.showFlutterFramesChart.value, isTrue); + }); + + test('stores values and reads them on init', () async { + storage.values.clear(); + + // Remember original values. + final showFramesChart = controller.showFlutterFramesChart.value; + + // Flip the values in controller. + controller.showFlutterFramesChart.value = !showFramesChart; + + // Check the values are stored. + expect(storage.values, hasLength(1)); + + // Reload the values from storage. + await controller.init(); + + // Check they did not change back to default. + expect( + controller.showFlutterFramesChart.value, + !showFramesChart, + ); + + // Flip the values in storage. + for (var key in storage.values.keys) { + storage.values[key] = (!(storage.values[key] == 'true')).toString(); + } + + // Reload the values from storage. + await controller.init(); + + // Check they flipped values are loaded. + expect( + controller.showFlutterFramesChart.value, + showFramesChart, + ); + }); + }); }
diff --git a/packages/devtools_test/lib/src/mocks/fake_vm_service_wrapper.dart b/packages/devtools_test/lib/src/mocks/fake_vm_service_wrapper.dart index 8f2951c..3af5654 100644 --- a/packages/devtools_test/lib/src/mocks/fake_vm_service_wrapper.dart +++ b/packages/devtools_test/lib/src/mocks/fake_vm_service_wrapper.dart
@@ -403,8 +403,7 @@ ) async { final httpProfile = await getHttpProfile(isolateId); return Future.value( - httpProfile.requests - .firstWhereOrNull((request) => request.id == id), + httpProfile.requests.firstWhereOrNull((request) => request.id == id), ); }
diff --git a/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart b/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart index a731892..f31922b 100644 --- a/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart +++ b/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart
@@ -43,6 +43,9 @@ .thenReturn(ValueNotifier<bool>(true)); when(timelineEventsController.legacyController) .thenReturn(legacyTimelineEventsController); + when(timelineEventsController.status).thenReturn( + ValueNotifier<EventsControllerStatus>(EventsControllerStatus.empty), + ); when(legacyTimelineEventsController.searchMatches) .thenReturn(const FixedValueListenable<List<TimelineEvent>>([])); when(legacyTimelineEventsController.searchInProgressNotifier)