From adeae74979fb72fe98e4cbc6e985169d2f5c936b Mon Sep 17 00:00:00 2001 From: Derek Xu Date: Tue, 3 Jun 2025 16:25:59 -0400 Subject: [PATCH 1/4] Add Queued Microtasks tab to Performance View --- packages/devtools_app/lib/devtools_app.dart | 1 + .../queued_microtasks_controller.dart | 90 ++++++ .../queued_microtasks_view.dart | 278 ++++++++++++++++++ .../performance/performance_controller.dart | 9 + .../performance/performance_screen.dart | 21 +- .../performance/tabbed_performance_view.dart | 27 ++ .../lib/src/service/vm_flags.dart | 3 + .../constants/_performance_constants.dart | 1 + .../release_notes/NEXT_RELEASE_NOTES.md | 6 +- .../queued_microtasks_controller_test.dart | 36 +++ .../tabbed_performance_view_test.dart | 166 +++++++++-- .../scenes/performance/default.dart | 2 +- .../performance/sample_performance_data.dart | 12 + .../lib/src/mocks/fake_service_manager.dart | 2 + .../src/mocks/fake_vm_service_wrapper.dart | 13 + .../lib/src/mocks/generated.dart | 1 + .../src/mocks/generated_mocks_factories.dart | 6 + 17 files changed, 653 insertions(+), 21 deletions(-) create mode 100644 packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_controller.dart create mode 100644 packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_view.dart create mode 100644 packages/devtools_app/test/screens/performance/queued_microtasks/queued_microtasks_controller_test.dart diff --git a/packages/devtools_app/lib/devtools_app.dart b/packages/devtools_app/lib/devtools_app.dart index fd801656ad5..d68416fba79 100644 --- a/packages/devtools_app/lib/devtools_app.dart +++ b/packages/devtools_app/lib/devtools_app.dart @@ -41,6 +41,7 @@ export 'src/screens/performance/panes/flutter_frames/flutter_frame_model.dart'; export 'src/screens/performance/panes/flutter_frames/flutter_frames_chart.dart'; export 'src/screens/performance/panes/flutter_frames/flutter_frames_controller.dart'; export 'src/screens/performance/panes/frame_analysis/frame_analysis_model.dart'; +export 'src/screens/performance/panes/queued_microtasks/queued_microtasks_controller.dart'; export 'src/screens/performance/panes/rebuild_stats/rebuild_stats_controller.dart'; export 'src/screens/performance/panes/rebuild_stats/rebuild_stats_model.dart'; export 'src/screens/performance/panes/timeline_events/perfetto/tracing/model.dart'; diff --git a/packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_controller.dart b/packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_controller.dart new file mode 100644 index 00000000000..b44027c3baa --- /dev/null +++ b/packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_controller.dart @@ -0,0 +1,90 @@ +// Copyright 2025 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +import 'dart:async'; + +import 'package:devtools_app_shared/utils.dart'; +import 'package:flutter/foundation.dart'; +import 'package:vm_service/vm_service.dart'; + +import '../../../../shared/globals.dart'; +import '../../../../shared/utils/future_work_tracker.dart'; +import '../../performance_controller.dart'; +import '../../performance_model.dart'; +import '../flutter_frames/flutter_frame_model.dart'; + +enum QueuedMicrotasksControllerStatus { empty, refreshing, ready } + +class QueuedMicrotasksController extends PerformanceFeatureController + with AutoDisposeControllerMixin { + QueuedMicrotasksController(super.controller) { + addAutoDisposeListener(_refreshWorkTracker.active, () { + final active = _refreshWorkTracker.active.value; + if (active) { + _status.value = QueuedMicrotasksControllerStatus.refreshing; + } else { + _status.value = QueuedMicrotasksControllerStatus.ready; + } + }); + } + + final _status = ValueNotifier( + QueuedMicrotasksControllerStatus.empty, + ); + ValueListenable get status => _status; + + final _queuedMicrotasks = ValueNotifier(null); + ValueListenable get queuedMicrotasks => _queuedMicrotasks; + + final _selectedMicrotask = ValueNotifier(null); + ValueListenable get selectedMicrotask => _selectedMicrotask; + + final _refreshWorkTracker = FutureWorkTracker(); + + @override + void onBecomingActive() {} + + Future refresh() => _refreshWorkTracker.track(() async { + _selectedMicrotask.value = null; + + final isolateId = serviceConnection + .serviceManager + .isolateManager + .selectedIsolate + .value! + .id!; + final queuedMicrotasks = await serviceConnection.serviceManager.service! + .getQueuedMicrotasks(isolateId); + _queuedMicrotasks.value = queuedMicrotasks; + + return; + }); + + void setSelectedMicrotask(Microtask? microtask) { + _selectedMicrotask.value = microtask; + } + + @override + Future handleSelectedFrame(FlutterFrame frame) async {} + + @override + Future setOfflineData(OfflinePerformanceData offlineData) async {} + + @override + Future clearData({bool partial = false}) async { + _selectedMicrotask.value = null; + _queuedMicrotasks.value = null; + } + + @override + void dispose() { + _status.dispose(); + _queuedMicrotasks.dispose(); + _selectedMicrotask.dispose(); + _refreshWorkTracker + ..clear() + ..dispose(); + super.dispose(); + } +} diff --git a/packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_view.dart b/packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_view.dart new file mode 100644 index 00000000000..225b66fba5b --- /dev/null +++ b/packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_view.dart @@ -0,0 +1,278 @@ +// Copyright 2025 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +import 'package:devtools_app_shared/ui.dart'; +import 'package:devtools_app_shared/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart' show DateFormat; +import 'package:vm_service/vm_service.dart'; + +import '../../../../shared/analytics/constants.dart' as gac; +import '../../../../shared/primitives/utils.dart' show SortDirection; +import '../../../../shared/table/table.dart' show FlatTable; +import '../../../../shared/table/table_data.dart'; +import '../../../../shared/ui/common_widgets.dart' show RefreshButton; +import 'queued_microtasks_controller.dart'; + +class RefreshQueuedMicrotasksButton extends StatelessWidget { + const RefreshQueuedMicrotasksButton({ + super.key, + required QueuedMicrotasksController controller, + }) : _controller = controller; + + final QueuedMicrotasksController _controller; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: _controller.status, + builder: (_, status, _) { + return RefreshButton( + iconOnly: true, + outlined: false, + onPressed: status == QueuedMicrotasksControllerStatus.refreshing + ? null + : _controller.refresh, + tooltip: + "Take a new snapshot of the selected isolate's microtask queue.", + gaScreen: gac.performance, + gaSelection: gac.PerformanceEvents.refreshQueuedMicrotasks.name, + ); + }, + ); + } +} + +class QueuedMicrotasksTabControls extends StatelessWidget { + const QueuedMicrotasksTabControls({ + super.key, + required QueuedMicrotasksController controller, + }) : _controller = controller; + + final QueuedMicrotasksController _controller; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [RefreshQueuedMicrotasksButton(controller: _controller)], + ); + } +} + +class RefreshQueuedMicrotasksInstructions extends StatelessWidget { + const RefreshQueuedMicrotasksInstructions({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: RichText( + text: TextSpan( + style: Theme.of(context).regularTextStyle, + children: [ + const TextSpan(text: 'Click the refresh button '), + WidgetSpan(child: Icon(Icons.refresh, size: defaultIconSize)), + const TextSpan( + text: + " to take a new snapshot of the selected isolate's " + 'microtask queue.', + ), + ], + ), + ), + ); + } +} + +// In the response returned by the VM Service, microtasks are sorted in +// ascending order of when they will be dequeued, i.e. the microtask that will +// run earliest is at index 0 of the returned list. We use those indices of the +// returned list to sort the entries of the microtask selector, so that they +// they also appear in ascending order of when they will be dequeued. +typedef IndexedMicrotask = (int, Microtask); + +class _MicrotaskIdColumn extends ColumnData { + _MicrotaskIdColumn() + : super.wide('Microtask ID', alignment: ColumnAlignment.center); + + @override + int getValue(IndexedMicrotask indexedMicrotask) => indexedMicrotask.$1; + + @override + String getDisplayValue(IndexedMicrotask indexedMicrotask) => + indexedMicrotask.$2.id!.toString(); +} + +class QueuedMicrotaskSelector extends StatelessWidget { + const QueuedMicrotaskSelector({ + super.key, + required List indexedMicrotasks, + required void Function(Microtask?) setSelectedMicrotask, + }) : _indexedMicrotasks = indexedMicrotasks, + _setSelectedMicrotask = setSelectedMicrotask; + + static final _idColumn = _MicrotaskIdColumn(); + final List _indexedMicrotasks; + final void Function(Microtask?) _setSelectedMicrotask; + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: FlatTable( + keyFactory: (IndexedMicrotask microtask) => + ValueKey(microtask.$1), + data: _indexedMicrotasks, + dataKey: 'queued-microtask-selector', + columns: [_idColumn], + defaultSortColumn: _idColumn, + defaultSortDirection: SortDirection.ascending, + onItemSelected: (indexedMicrotask) => + _setSelectedMicrotask(indexedMicrotask?.$2), + ), + ), + ], + ); +} + +class StackTraceView extends StatelessWidget { + const StackTraceView({super.key, required selectedMicrotask}) + : _selectedMicrotask = selectedMicrotask; + + final Microtask? _selectedMicrotask; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + children: [ + SizedBox.fromSize( + size: Size.fromHeight(defaultHeaderHeight), + child: Container( + decoration: BoxDecoration( + border: Border(bottom: defaultBorderSide(theme)), + ), + padding: const EdgeInsets.only(left: defaultSpacing), + alignment: Alignment.centerLeft, + child: const Row( + children: [ + Text('Stack trace captured when microtask was enqueued'), + ], + ), + ), + ), + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: denseRowSpacing, + horizontal: defaultSpacing, + ), + child: SelectableText( + style: theme.fixedFontStyle, + _selectedMicrotask!.stackTrace.toString(), + ), + ), + ), + ], + ), + ], + ); + } +} + +class QueuedMicrotasksTabView extends StatefulWidget { + const QueuedMicrotasksTabView({super.key, required this.controller}); + + final QueuedMicrotasksController controller; + + @override + State createState() => + _QueuedMicrotasksTabViewState(); +} + +class _QueuedMicrotasksTabViewState extends State + with AutoDisposeMixin { + static final _dateTimeFormat = DateFormat('HH:mm:ss.SSS (MM/dd/yy)'); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: widget.controller.status, + builder: (context, status, _) { + if (status == QueuedMicrotasksControllerStatus.empty) { + return const RefreshQueuedMicrotasksInstructions(); + } else if (status == QueuedMicrotasksControllerStatus.refreshing) { + return Center( + child: Text( + style: Theme.of(context).regularTextStyle, + 'Refreshing...', + ), + ); + } else { + return ValueListenableBuilder( + valueListenable: widget.controller.queuedMicrotasks, + builder: (_, queuedMicrotasks, _) { + assert(queuedMicrotasks != null); + + final indexedMicrotasks = queuedMicrotasks!.microtasks!.indexed + .cast() + .toList(); + final formattedTimestamp = _dateTimeFormat.format( + DateTime.fromMicrosecondsSinceEpoch( + queuedMicrotasks.timestamp!, + ), + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: denseRowSpacing, + horizontal: defaultSpacing, + ), + child: Text( + 'Viewing snapshot that was taken at $formattedTimestamp.', + ), + ), + Expanded( + child: SplitPane( + axis: Axis.horizontal, + initialFractions: const [0.15, 0.85], + children: [ + QueuedMicrotaskSelector( + indexedMicrotasks: indexedMicrotasks, + setSelectedMicrotask: + widget.controller.setSelectedMicrotask, + ), + ValueListenableBuilder( + valueListenable: widget.controller.selectedMicrotask, + builder: (_, selectedMicrotask, _) => + selectedMicrotask == null + ? const Center( + child: Text( + 'Select a microtask ID on the left ' + 'to see information about the ' + 'corresponding microtask.', + ), + ) + : StackTraceView( + selectedMicrotask: selectedMicrotask, + ), + ), + ], + ), + ), + ], + ); + }, + ); + } + }, + ); + } +} 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 cfb4469afca..914ba111ba4 100644 --- a/packages/devtools_app/lib/src/screens/performance/performance_controller.dart +++ b/packages/devtools_app/lib/src/screens/performance/performance_controller.dart @@ -22,6 +22,7 @@ import '../../shared/offline/offline_data.dart'; import 'panes/controls/enhance_tracing/enhance_tracing_controller.dart'; import 'panes/flutter_frames/flutter_frame_model.dart'; import 'panes/flutter_frames/flutter_frames_controller.dart'; +import 'panes/queued_microtasks/queued_microtasks_controller.dart'; import 'panes/rebuild_stats/rebuild_stats_controller.dart'; import 'panes/rebuild_stats/rebuild_stats_model.dart'; import 'panes/timeline_events/timeline_events_controller.dart'; @@ -53,6 +54,8 @@ class PerformanceController extends DevToolsScreenController late final TimelineEventsController timelineEventsController; + late final QueuedMicrotasksController queuedMicrotasksController; + late final RebuildStatsController rebuildStatsController; late List _featureControllers; @@ -90,6 +93,8 @@ class PerformanceController extends DevToolsScreenController Future get initialized => _initialized.future; final _initialized = Completer(); + final isQueuedMicrotasksFeatureActive = ValueNotifier(false); + @override void init() { super.init(); @@ -97,10 +102,12 @@ class PerformanceController extends DevToolsScreenController // only create a controller when it is needed, flutterFramesController = FlutterFramesController(this); timelineEventsController = TimelineEventsController(this); + queuedMicrotasksController = QueuedMicrotasksController(this); rebuildStatsController = RebuildStatsController(this); _featureControllers = [ flutterFramesController, timelineEventsController, + queuedMicrotasksController, rebuildStatsController, ]; @@ -251,6 +258,8 @@ class PerformanceController extends DevToolsScreenController featureController != null && c == featureController, ), ); + isQueuedMicrotasksFeatureActive.value = + queuedMicrotasksController.isActiveFeature; } /// Clears the timeline data currently stored by the controller as well the 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 ac10baf610d..6780f97ffbf 100644 --- a/packages/devtools_app/lib/src/screens/performance/performance_screen.dart +++ b/packages/devtools_app/lib/src/screens/performance/performance_screen.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; +import 'package:flutter/foundation.dart' show ValueListenable; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:provider/provider.dart'; @@ -35,13 +36,18 @@ class PerformanceScreen extends Screen { @override String get docPageId => id; + final _showIsolateSelector = ValueNotifier(false); + + @override + ValueListenable get showIsolateSelector => _showIsolateSelector; + @override Widget buildScreenBody(BuildContext context) { if (serviceConnection.serviceManager.connectedApp?.isDartWebAppNow ?? false) { return const WebPerformanceScreenBody(); } - return const PerformanceScreenBody(); + return PerformanceScreenBody(showIsolateSelector: _showIsolateSelector); } @override @@ -51,7 +57,14 @@ class PerformanceScreen extends Screen { } class PerformanceScreenBody extends StatefulWidget { - const PerformanceScreenBody({super.key}); + const PerformanceScreenBody({ + super.key, + // This allows the body to modify the value that gets returned by the + // enclosing [PerformanceScreen]'s `showIsolateSelector` [ValueListenable]. + required this.showIsolateSelector, + }); + + final ValueNotifier showIsolateSelector; @override PerformanceScreenBodyState createState() => PerformanceScreenBodyState(); @@ -68,6 +81,10 @@ class PerformanceScreenBodyState extends State controller = screenControllers.lookup(); addAutoDisposeListener(offlineDataController.showingOfflineData); addAutoDisposeListener(controller.loadingOfflineData); + addAutoDisposeListener(controller.isQueuedMicrotasksFeatureActive, () { + widget.showIsolateSelector.value = + controller.isQueuedMicrotasksFeatureActive.value; + }); } @override 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 05eef54cd1c..ac1039cb812 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 @@ -8,6 +8,7 @@ import 'package:collection/collection.dart'; import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/material.dart'; +import '../../service/vm_flags.dart' as vm_flags; import '../../shared/analytics/constants.dart' as gac; import '../../shared/globals.dart'; import '../../shared/ui/common_widgets.dart'; @@ -15,6 +16,7 @@ import '../../shared/ui/tab.dart'; import 'panes/flutter_frames/flutter_frame_model.dart'; import 'panes/flutter_frames/flutter_frames_controller.dart'; import 'panes/frame_analysis/frame_analysis.dart'; +import 'panes/queued_microtasks/queued_microtasks_view.dart'; import 'panes/rebuild_stats/rebuild_stats.dart'; import 'panes/timeline_events/timeline_events_view.dart'; import 'performance_controller.dart'; @@ -69,9 +71,20 @@ class _TabbedPerformanceViewState extends State offlineData.rebuildCountModel != null; } + // The value of the `--profile-microtasks` VM flag cannot be modified once + // the VM has started running. + final showQueuedMicrotasks = + !isOffline && + serviceConnection.vmFlagManager + .flag(vm_flags.profileMicrotasks) + ?.value + .valueAsString == + 'true'; + final tabsAndControllers = _generateTabs( showFrameAnalysis: showFrameAnalysis, showRebuildStats: showRebuildStats, + showQueuedMicrotasks: showQueuedMicrotasks, ); final tabs = tabsAndControllers .map((t) => (tab: t.tab, tabView: t.tabView)) @@ -116,6 +129,7 @@ class _TabbedPerformanceViewState extends State _generateTabs({ required bool showFrameAnalysis, required bool showRebuildStats, + required bool showQueuedMicrotasks, }) { if (showFrameAnalysis || showRebuildStats) { assert(serviceConnection.serviceManager.connectedApp!.isFlutterAppNow!); @@ -163,6 +177,19 @@ class _TabbedPerformanceViewState extends State ), featureController: controller.timelineEventsController, ), + if (showQueuedMicrotasks) + ( + tab: _buildTab( + tabName: 'Queued Microtasks', + trailing: QueuedMicrotasksTabControls( + controller: controller.queuedMicrotasksController, + ), + ), + tabView: QueuedMicrotasksTabView( + controller: controller.queuedMicrotasksController, + ), + featureController: controller.queuedMicrotasksController, + ), ]; } diff --git a/packages/devtools_app/lib/src/service/vm_flags.dart b/packages/devtools_app/lib/src/service/vm_flags.dart index 3e803ca3603..338097e28ac 100644 --- a/packages/devtools_app/lib/src/service/vm_flags.dart +++ b/packages/devtools_app/lib/src/service/vm_flags.dart @@ -16,6 +16,9 @@ const profiler = 'profiler'; // Defined in SDK: https://github.com/dart-lang/sdk/blob/master/runtime/vm/profiler.cc#L36 const profilePeriod = 'profile_period'; +// Defined in SDK: https://github.com/dart-lang/sdk/blob/main/runtime/vm/microtask_mirror_queues.cc#L18-L23. +const profileMicrotasks = 'profile_microtasks'; + class VmFlagManager with DisposerMixin { VmServiceWrapper get service => _service; late VmServiceWrapper _service; diff --git a/packages/devtools_app/lib/src/shared/analytics/constants/_performance_constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants/_performance_constants.dart index 1b548e82749..2982704d395 100644 --- a/packages/devtools_app/lib/src/shared/analytics/constants/_performance_constants.dart +++ b/packages/devtools_app/lib/src/shared/analytics/constants/_performance_constants.dart @@ -7,6 +7,7 @@ part of '../constants.dart'; enum PerformanceEvents { refreshTimelineEvents, includeCpuSamplesInTimeline, + refreshQueuedMicrotasks, performanceOverlay, framesChartVisibility, selectFlutterFrame, diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 02466fc7044..8e6a768cf48 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -23,7 +23,11 @@ TODO: Remove this section if there are not any general updates. ## Performance updates -TODO: Remove this section if there are not any general updates. +- Added a Queued Microtasks tab to the Performance View, which allows a user to + see details about the microtasks scheduled in an isolate's microtask queue. + This tab currently only appears when DevTools is connected to a Flutter or + Dart app started with `--profile-microtasks`. - + [#9239](https://github.com/flutter/devtools/pull/9239). ## CPU profiler updates diff --git a/packages/devtools_app/test/screens/performance/queued_microtasks/queued_microtasks_controller_test.dart b/packages/devtools_app/test/screens/performance/queued_microtasks/queued_microtasks_controller_test.dart new file mode 100644 index 00000000000..6a904b8871b --- /dev/null +++ b/packages/devtools_app/test/screens/performance/queued_microtasks/queued_microtasks_controller_test.dart @@ -0,0 +1,36 @@ +// Copyright 2025 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +import 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_app_shared/utils.dart'; +import 'package:devtools_test/devtools_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../../test_infra/test_data/performance/sample_performance_data.dart'; + +void main() { + late FakeServiceConnectionManager fakeServiceConnection; + late QueuedMicrotasksController controller; + + group('QueuedMicrotasksController', () { + setUp(() { + fakeServiceConnection = FakeServiceConnectionManager( + service: FakeServiceManager.createFakeService( + queuedMicrotasks: testQueuedMicrotasks, + ), + ); + setGlobal(ServiceConnectionManager, fakeServiceConnection); + + controller = QueuedMicrotasksController( + createMockPerformanceControllerWithDefaults(), + ); + }); + + test('refresh', () async { + await controller.refresh(); + expect(controller.status.value, QueuedMicrotasksControllerStatus.ready); + expect(controller.queuedMicrotasks.value, testQueuedMicrotasks); + }); + }); +} diff --git a/packages/devtools_app/test/screens/performance/tabbed_performance_view_test.dart b/packages/devtools_app/test/screens/performance/tabbed_performance_view_test.dart index 66239af683d..de2d634ff10 100644 --- a/packages/devtools_app/test/screens/performance/tabbed_performance_view_test.dart +++ b/packages/devtools_app/test/screens/performance/tabbed_performance_view_test.dart @@ -4,6 +4,7 @@ import 'package:devtools_app/devtools_app.dart'; import 'package:devtools_app/src/screens/performance/panes/frame_analysis/frame_analysis.dart'; +import 'package:devtools_app/src/screens/performance/panes/queued_microtasks/queued_microtasks_view.dart'; import 'package:devtools_app/src/screens/performance/panes/rebuild_stats/rebuild_stats.dart'; import 'package:devtools_app/src/screens/performance/panes/timeline_events/perfetto/perfetto.dart'; import 'package:devtools_app/src/screens/performance/panes/timeline_events/timeline_events_view.dart'; @@ -24,12 +25,22 @@ void main() { late FakeServiceConnectionManager fakeServiceConnection; late MockPerformanceController controller; - Future setUpServiceManagerWithTimeline() async { + Future setUpServiceManagerWithTimeline({ + bool shouldEnableMicrotaskProfiling = false, + }) async { fakeServiceConnection = FakeServiceConnectionManager( service: FakeServiceManager.createFakeService( timelineData: perfettoVmTimeline, + vmFlags: [ + ( + flagName: profileMicrotasks, + value: shouldEnableMicrotaskProfiling ? 'true' : 'false', + ), + ], ), ); + await fakeServiceConnection.serviceManager.flagsInitialized.future; + final app = fakeServiceConnection.serviceManager.connectedApp!; mockConnectedApp( app, @@ -51,8 +62,7 @@ void main() { } group('TabbedPerformanceView', () { - setUp(() async { - await setUpServiceManagerWithTimeline(); + setUp(() { setGlobal( DevToolsEnvironmentParameters, ExternalDevToolsEnvironmentParameters(), @@ -105,21 +115,47 @@ void main() { const windowSize = Size(2225.0, 1000.0); - testWidgetsWithWindowSize('builds content successfully', windowSize, ( - WidgetTester tester, - ) async { - await tester.runAsync(() async { - await setUpServiceManagerWithTimeline(); - await pumpView(tester); + testWidgetsWithWindowSize( + 'builds content successfully when connected to a Flutter app and ' + 'microtask profiling is disabled', + windowSize, + (WidgetTester tester) async { + await tester.runAsync(() async { + await setUpServiceManagerWithTimeline(); + await pumpView(tester); - expect(find.byType(AnalyticsTabbedView), findsOneWidget); - expect(find.byType(DevToolsTab), findsNWidgets(3)); + expect(find.byType(AnalyticsTabbedView), findsOneWidget); + expect(find.byType(DevToolsTab), findsNWidgets(3)); - expect(find.text('Timeline Events'), findsOneWidget); - expect(find.text('Frame Analysis'), findsOneWidget); - expect(find.text('Rebuild Stats'), findsOneWidget); - }); - }); + expect(find.text('Timeline Events'), findsOneWidget); + expect(find.text('Frame Analysis'), findsOneWidget); + expect(find.text('Rebuild Stats'), findsOneWidget); + expect(find.text('Queued Microtasks'), findsNothing); + }); + }, + ); + + testWidgetsWithWindowSize( + 'builds content successfully when connected to a Flutter app and ' + 'microtask profiling is enabled', + windowSize, + (WidgetTester tester) async { + await tester.runAsync(() async { + await setUpServiceManagerWithTimeline( + shouldEnableMicrotaskProfiling: true, + ); + await pumpView(tester); + + expect(find.byType(AnalyticsTabbedView), findsOneWidget); + expect(find.byType(DevToolsTab), findsNWidgets(4)); + + expect(find.text('Frame Analysis'), findsOneWidget); + expect(find.text('Rebuild Stats'), findsOneWidget); + expect(find.text('Timeline Events'), findsOneWidget); + expect(find.text('Queued Microtasks'), findsOneWidget); + }); + }, + ); testWidgetsWithWindowSize( 'builds content for Frame Analysis tab with selected frame', @@ -208,7 +244,72 @@ void main() { ); testWidgetsWithWindowSize( - 'only shows Timeline Events tab for non-flutter app', + 'builds content for Queued Microtasks tab when microtask profiling is ' + 'enabled', + windowSize, + (WidgetTester tester) async { + await tester.runAsync(() async { + await setUpServiceManagerWithTimeline( + shouldEnableMicrotaskProfiling: true, + ); + + // First, we verify that instructions are shown in the Queued + // Microtasks tab when [controller.queuedMicrotasksController]'s + // status is empty. + + final mockQueuedMicrotasksController = + controller.queuedMicrotasksController + as MockQueuedMicrotasksController; + when(mockQueuedMicrotasksController.status).thenReturn( + const FixedValueListenable( + QueuedMicrotasksControllerStatus.empty, + ), + ); + + await pumpView(tester); + + expect(find.byType(AnalyticsTabbedView), findsOneWidget); + expect(find.byType(DevToolsTab), findsNWidgets(4)); + + await tester.tap(find.text('Queued Microtasks')); + await tester.pumpAndSettle(); + + expect(find.byType(RefreshQueuedMicrotasksButton), findsOneWidget); + expect(find.byType(QueuedMicrotasksTabView), findsOneWidget); + expect( + find.byType(RefreshQueuedMicrotasksInstructions), + findsOneWidget, + ); + + // Then, we verify that details about the queued microtasks are shown + // when [controller.queuedMicrotasksController]'s status is ready. + + when(mockQueuedMicrotasksController.status).thenReturn( + const FixedValueListenable( + QueuedMicrotasksControllerStatus.ready, + ), + ); + when( + mockQueuedMicrotasksController.selectedMicrotask, + ).thenReturn(FixedValueListenable(testMicrotask)); + when( + mockQueuedMicrotasksController.queuedMicrotasks, + ).thenReturn(FixedValueListenable(testQueuedMicrotasks)); + + await tester.tap(find.text('Timeline Events')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Queued Microtasks')); + await tester.pumpAndSettle(); + + expect(find.byType(QueuedMicrotaskSelector), findsOneWidget); + expect(find.byType(StackTraceView), findsOneWidget); + }); + }, + ); + + testWidgetsWithWindowSize( + 'only shows Timeline Events tab when connected to a non-Flutter app and ' + 'microtask profiling is disabled', windowSize, (WidgetTester tester) async { await tester.runAsync(() async { @@ -227,6 +328,37 @@ void main() { expect(find.byType(AnalyticsTabbedView), findsOneWidget); expect(find.byType(DevToolsTab), findsOneWidget); expect(find.text('Timeline Events'), findsOneWidget); + expect(find.text('Queued Microtasks'), findsNothing); + expect(find.text('Frame Analysis'), findsNothing); + expect(find.text('Rebuild Stats'), findsNothing); + }); + }, + ); + + testWidgetsWithWindowSize( + 'only shows Timeline Events and Queued Microtasks tabs when connected to ' + 'a non-flutter app and microtask profiling is enabled', + windowSize, + (WidgetTester tester) async { + await tester.runAsync(() async { + await setUpServiceManagerWithTimeline( + shouldEnableMicrotaskProfiling: true, + ); + final app = fakeServiceConnection.serviceManager.connectedApp!; + mockConnectedApp( + app, + isFlutterApp: false, + isProfileBuild: false, + isWebApp: false, + ); + when(app.flutterVersionNow).thenReturn(null); + + await pumpView(tester); + await tester.pumpAndSettle(); + expect(find.byType(AnalyticsTabbedView), findsOneWidget); + expect(find.byType(DevToolsTab), findsNWidgets(2)); + expect(find.text('Timeline Events'), findsOneWidget); + expect(find.text('Queued Microtasks'), findsOneWidget); expect(find.text('Frame Analysis'), findsNothing); expect(find.text('Rebuild Stats'), findsNothing); }); diff --git a/packages/devtools_app/test/test_infra/scenes/performance/default.dart b/packages/devtools_app/test/test_infra/scenes/performance/default.dart index aec9c04b805..69634d8bf78 100644 --- a/packages/devtools_app/test/test_infra/scenes/performance/default.dart +++ b/packages/devtools_app/test/test_infra/scenes/performance/default.dart @@ -21,7 +21,7 @@ class PerformanceDefaultScene extends Scene { @override Widget build(BuildContext context) { return wrapWithControllers( - const PerformanceScreenBody(), + PerformanceScreenBody(showIsolateSelector: ValueNotifier(false)), performance: controller, ); } diff --git a/packages/devtools_app/test/test_infra/test_data/performance/sample_performance_data.dart b/packages/devtools_app/test/test_infra/test_data/performance/sample_performance_data.dart index 8a9362ca124..b2ef0be0196 100644 --- a/packages/devtools_app/test/test_infra/test_data/performance/sample_performance_data.dart +++ b/packages/devtools_app/test/test_infra/test_data/performance/sample_performance_data.dart @@ -1359,3 +1359,15 @@ extension FlutterFrame6 on Never { endMicros: 713836331505, ); } + +final testMicrotask = Microtask.parse({ + 'type': 'Microtask', + 'id': 123, + 'stackTrace': 'stack trace', +}); + +final testQueuedMicrotasks = QueuedMicrotasks.parse({ + 'type': 'QueuedMicrotasks', + 'timestamp': DateTime(2001).microsecondsSinceEpoch, + 'microtasks': [testMicrotask], +}); diff --git a/packages/devtools_test/lib/src/mocks/fake_service_manager.dart b/packages/devtools_test/lib/src/mocks/fake_service_manager.dart index 278229aebb7..206eb16f740 100644 --- a/packages/devtools_test/lib/src/mocks/fake_service_manager.dart +++ b/packages/devtools_test/lib/src/mocks/fake_service_manager.dart @@ -116,6 +116,7 @@ class FakeServiceManager extends Fake static FakeVmServiceWrapper createFakeService({ PerfettoTimeline? timelineData, + QueuedMicrotasks? queuedMicrotasks, SocketProfile? socketProfile, HttpProfile? httpProfile, SamplesMemoryJson? memoryData, @@ -128,6 +129,7 @@ class FakeServiceManager extends Fake }) => FakeVmServiceWrapper( _flagManager, timelineData, + queuedMicrotasks, socketProfile, httpProfile, memoryData, 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 01e2fdd3838..60d2f987857 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 @@ -17,6 +17,7 @@ class FakeVmServiceWrapper extends Fake implements VmServiceWrapper { FakeVmServiceWrapper( this._vmFlagManager, this._timelineData, + this._queuedMicrotasks, this._socketProfile, this._httpProfile, this._memoryData, @@ -74,6 +75,7 @@ class FakeVmServiceWrapper extends Fake implements VmServiceWrapper { final VmFlagManager _vmFlagManager; final PerfettoTimeline? _timelineData; + final QueuedMicrotasks? _queuedMicrotasks; SocketProfile? _socketProfile; final List _startingSockets; HttpProfile? _httpProfile; @@ -372,6 +374,17 @@ class FakeVmServiceWrapper extends Fake implements VmServiceWrapper { @override Future clearVMTimeline() => Future.value(Success()); + @override + Future getQueuedMicrotasks(String isolateId) { + if (_queuedMicrotasks == null) { + throw StateError( + 'An argument to the queuedMicrotasks parameter was not provided to ' + 'createFakeService', + ); + } + return Future.value(_queuedMicrotasks); + } + @override Future getClassList(String isolateId) { return Future.value(_classList ?? ClassList(classes: [])); diff --git a/packages/devtools_test/lib/src/mocks/generated.dart b/packages/devtools_test/lib/src/mocks/generated.dart index a02acb34b43..5ab1c7ddc4c 100644 --- a/packages/devtools_test/lib/src/mocks/generated.dart +++ b/packages/devtools_test/lib/src/mocks/generated.dart @@ -21,6 +21,7 @@ import 'package:vm_service/vm_service.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), MockSpec(), MockSpec(), MockSpec(), 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 f710b85b490..843b15c89ee 100644 --- a/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart +++ b/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart @@ -20,6 +20,7 @@ import 'generated.mocks.dart'; MockPerformanceController createMockPerformanceControllerWithDefaults() { final controller = MockPerformanceController(); final timelineEventsController = MockTimelineEventsController(); + final queuedMicrotasksController = MockQueuedMicrotasksController(); final flutterFramesController = MockFlutterFramesController(); when( controller.enhanceTracingController, @@ -48,6 +49,11 @@ MockPerformanceController createMockPerformanceControllerWithDefaults() { ValueNotifier(EventsControllerStatus.empty), ); + // Stubs for Queued Microtasks feature. + when( + controller.queuedMicrotasksController, + ).thenReturn(queuedMicrotasksController); + // Stubs for Rebuild Count feature when(controller.rebuildCountModel).thenReturn(RebuildCountModel()); when( From 1590b9f925847d5788c1a1ccaeb692e0fbadfaeb Mon Sep 17 00:00:00 2001 From: Derek Xu Date: Tue, 10 Jun 2025 15:48:09 -0400 Subject: [PATCH 2/4] Address comments --- .../queued_microtasks_controller.dart | 6 +- .../queued_microtasks_view.dart | 146 +++++++----------- .../performance/tabbed_performance_view.dart | 2 +- .../lib/src/service/vm_flags.dart | 2 +- packages/devtools_app/pubspec.yaml | 4 +- .../queued_microtasks_controller_test.dart | 4 + .../tabbed_performance_view_test.dart | 27 ++-- 7 files changed, 83 insertions(+), 108 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_controller.dart b/packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_controller.dart index b44027c3baa..58129c9c636 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_controller.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_controller.dart @@ -29,16 +29,16 @@ class QueuedMicrotasksController extends PerformanceFeatureController }); } + ValueListenable get status => _status; final _status = ValueNotifier( QueuedMicrotasksControllerStatus.empty, ); - ValueListenable get status => _status; - final _queuedMicrotasks = ValueNotifier(null); ValueListenable get queuedMicrotasks => _queuedMicrotasks; + final _queuedMicrotasks = ValueNotifier(null); - final _selectedMicrotask = ValueNotifier(null); ValueListenable get selectedMicrotask => _selectedMicrotask; + final _selectedMicrotask = ValueNotifier(null); final _refreshWorkTracker = FutureWorkTracker(); diff --git a/packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_view.dart b/packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_view.dart index 225b66fba5b..ed3d1277b18 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_view.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_view.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 or at https://developers.google.com/open-source/licenses/bsd. +import 'package:collection/collection.dart' show ListExtensions; import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/material.dart'; @@ -12,28 +13,26 @@ import '../../../../shared/analytics/constants.dart' as gac; import '../../../../shared/primitives/utils.dart' show SortDirection; import '../../../../shared/table/table.dart' show FlatTable; import '../../../../shared/table/table_data.dart'; -import '../../../../shared/ui/common_widgets.dart' show RefreshButton; +import '../../../../shared/ui/common_widgets.dart' + show CenteredMessage, RefreshButton; import 'queued_microtasks_controller.dart'; class RefreshQueuedMicrotasksButton extends StatelessWidget { - const RefreshQueuedMicrotasksButton({ - super.key, - required QueuedMicrotasksController controller, - }) : _controller = controller; + const RefreshQueuedMicrotasksButton({super.key, required this.controller}); - final QueuedMicrotasksController _controller; + final QueuedMicrotasksController controller; @override Widget build(BuildContext context) { return ValueListenableBuilder( - valueListenable: _controller.status, + valueListenable: controller.status, builder: (_, status, _) { return RefreshButton( iconOnly: true, outlined: false, onPressed: status == QueuedMicrotasksControllerStatus.refreshing ? null - : _controller.refresh, + : controller.refresh, tooltip: "Take a new snapshot of the selected isolate's microtask queue.", gaScreen: gac.performance, @@ -44,23 +43,6 @@ class RefreshQueuedMicrotasksButton extends StatelessWidget { } } -class QueuedMicrotasksTabControls extends StatelessWidget { - const QueuedMicrotasksTabControls({ - super.key, - required QueuedMicrotasksController controller, - }) : _controller = controller; - - final QueuedMicrotasksController _controller; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [RefreshQueuedMicrotasksButton(controller: _controller)], - ); - } -} - class RefreshQueuedMicrotasksInstructions extends StatelessWidget { const RefreshQueuedMicrotasksInstructions({super.key}); @@ -85,60 +67,52 @@ class RefreshQueuedMicrotasksInstructions extends StatelessWidget { } } -// In the response returned by the VM Service, microtasks are sorted in -// ascending order of when they will be dequeued, i.e. the microtask that will -// run earliest is at index 0 of the returned list. We use those indices of the -// returned list to sort the entries of the microtask selector, so that they -// they also appear in ascending order of when they will be dequeued. -typedef IndexedMicrotask = (int, Microtask); +/// In the response returned by the VM Service, microtasks are sorted in +/// ascending order of when they will be dequeued, i.e. the microtask that will +/// run earliest is at index 0 of the returned list. We use those indices of the +/// returned list to sort the entries of the microtask selector, so that they +/// they also appear in ascending order of when they will be dequeued. +typedef IndexedMicrotask = ({int index, Microtask microtask}); class _MicrotaskIdColumn extends ColumnData { _MicrotaskIdColumn() : super.wide('Microtask ID', alignment: ColumnAlignment.center); @override - int getValue(IndexedMicrotask indexedMicrotask) => indexedMicrotask.$1; + int getValue(IndexedMicrotask indexedMicrotask) => indexedMicrotask.index; @override String getDisplayValue(IndexedMicrotask indexedMicrotask) => - indexedMicrotask.$2.id!.toString(); + indexedMicrotask.microtask.id!.toString(); } class QueuedMicrotaskSelector extends StatelessWidget { const QueuedMicrotaskSelector({ super.key, required List indexedMicrotasks, - required void Function(Microtask?) setSelectedMicrotask, + required void Function(Microtask?) onMicrotaskSelected, }) : _indexedMicrotasks = indexedMicrotasks, - _setSelectedMicrotask = setSelectedMicrotask; + _setSelectedMicrotask = onMicrotaskSelected; static final _idColumn = _MicrotaskIdColumn(); final List _indexedMicrotasks; final void Function(Microtask?) _setSelectedMicrotask; @override - Widget build(BuildContext context) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: FlatTable( - keyFactory: (IndexedMicrotask microtask) => - ValueKey(microtask.$1), - data: _indexedMicrotasks, - dataKey: 'queued-microtask-selector', - columns: [_idColumn], - defaultSortColumn: _idColumn, - defaultSortDirection: SortDirection.ascending, - onItemSelected: (indexedMicrotask) => - _setSelectedMicrotask(indexedMicrotask?.$2), - ), - ), - ], + Widget build(BuildContext context) => FlatTable( + keyFactory: (IndexedMicrotask microtask) => ValueKey(microtask.index), + data: _indexedMicrotasks, + dataKey: 'queued-microtask-selector', + columns: [_idColumn], + defaultSortColumn: _idColumn, + defaultSortDirection: SortDirection.ascending, + onItemSelected: (indexedMicrotask) => + _setSelectedMicrotask(indexedMicrotask?.microtask), ); } -class StackTraceView extends StatelessWidget { - const StackTraceView({super.key, required selectedMicrotask}) +class MicrotaskStackTraceView extends StatelessWidget { + const MicrotaskStackTraceView({super.key, required selectedMicrotask}) : _selectedMicrotask = selectedMicrotask; final Microtask? _selectedMicrotask; @@ -149,14 +123,11 @@ class StackTraceView extends StatelessWidget { return Column( children: [ - SizedBox.fromSize( - size: Size.fromHeight(defaultHeaderHeight), - child: Container( - decoration: BoxDecoration( - border: Border(bottom: defaultBorderSide(theme)), - ), - padding: const EdgeInsets.only(left: defaultSpacing), - alignment: Alignment.centerLeft, + Container( + height: defaultHeaderHeight, + padding: const EdgeInsets.only(left: defaultSpacing), + alignment: Alignment.centerLeft, + child: OutlineDecoration.onlyBottom( child: const Row( children: [ Text('Stack trace captured when microtask was enqueued'), @@ -207,20 +178,20 @@ class _QueuedMicrotasksTabViewState extends State if (status == QueuedMicrotasksControllerStatus.empty) { return const RefreshQueuedMicrotasksInstructions(); } else if (status == QueuedMicrotasksControllerStatus.refreshing) { - return Center( - child: Text( - style: Theme.of(context).regularTextStyle, - 'Refreshing...', - ), - ); + return const CenteredMessage(message: 'Refreshing...'); } else { return ValueListenableBuilder( valueListenable: widget.controller.queuedMicrotasks, builder: (_, queuedMicrotasks, _) { assert(queuedMicrotasks != null); - - final indexedMicrotasks = queuedMicrotasks!.microtasks!.indexed - .cast() + if (queuedMicrotasks == null) { + return const CenteredMessage(message: 'Unexpected null value'); + } + + final indexedMicrotasks = queuedMicrotasks.microtasks! + .mapIndexed( + (index, microtask) => (index: index, microtask: microtask), + ) .toList(); final formattedTimestamp = _dateTimeFormat.format( DateTime.fromMicrosecondsSinceEpoch( @@ -244,25 +215,28 @@ class _QueuedMicrotasksTabViewState extends State axis: Axis.horizontal, initialFractions: const [0.15, 0.85], children: [ - QueuedMicrotaskSelector( - indexedMicrotasks: indexedMicrotasks, - setSelectedMicrotask: - widget.controller.setSelectedMicrotask, + OutlineDecoration( + child: QueuedMicrotaskSelector( + indexedMicrotasks: indexedMicrotasks, + onMicrotaskSelected: + widget.controller.setSelectedMicrotask, + ), ), ValueListenableBuilder( valueListenable: widget.controller.selectedMicrotask, builder: (_, selectedMicrotask, _) => - selectedMicrotask == null - ? const Center( - child: Text( - 'Select a microtask ID on the left ' - 'to see information about the ' - 'corresponding microtask.', - ), - ) - : StackTraceView( - selectedMicrotask: selectedMicrotask, - ), + OutlineDecoration( + child: selectedMicrotask == null + ? const CenteredMessage( + message: + 'Select a microtask ID on the left ' + 'to see information about the ' + 'corresponding microtask.', + ) + : MicrotaskStackTraceView( + selectedMicrotask: selectedMicrotask, + ), + ), ), ], ), 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 ac1039cb812..6eeb3ce33d4 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 @@ -181,7 +181,7 @@ class _TabbedPerformanceViewState extends State ( tab: _buildTab( tabName: 'Queued Microtasks', - trailing: QueuedMicrotasksTabControls( + trailing: RefreshQueuedMicrotasksButton( controller: controller.queuedMicrotasksController, ), ), diff --git a/packages/devtools_app/lib/src/service/vm_flags.dart b/packages/devtools_app/lib/src/service/vm_flags.dart index 338097e28ac..71d9955aed0 100644 --- a/packages/devtools_app/lib/src/service/vm_flags.dart +++ b/packages/devtools_app/lib/src/service/vm_flags.dart @@ -16,7 +16,7 @@ const profiler = 'profiler'; // Defined in SDK: https://github.com/dart-lang/sdk/blob/master/runtime/vm/profiler.cc#L36 const profilePeriod = 'profile_period'; -// Defined in SDK: https://github.com/dart-lang/sdk/blob/main/runtime/vm/microtask_mirror_queues.cc#L18-L23. +// Defined in SDK: https://github.com/dart-lang/sdk/blob/main/runtime/vm/microtask_mirror_queues.cc. const profileMicrotasks = 'profile_microtasks'; class VmFlagManager with DisposerMixin { diff --git a/packages/devtools_app/pubspec.yaml b/packages/devtools_app/pubspec.yaml index 973f709784d..5af4ae32bd4 100644 --- a/packages/devtools_app/pubspec.yaml +++ b/packages/devtools_app/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: collection: ^1.15.0 dap: ^1.1.0 dart_service_protocol_shared: ^0.0.3 - dds_service_extensions: ^2.0.0 + dds_service_extensions: ^2.0.2 devtools_app_shared: devtools_extensions: devtools_shared: @@ -53,7 +53,7 @@ dependencies: stack_trace: ^1.12.0 string_scanner: ^1.4.0 unified_analytics: ^7.0.0 - vm_service: '>=15.0.0 <16.0.0' + vm_service: ^15.0.2 vm_service_protos: ^1.0.0 vm_snapshot_analysis: ^0.7.6 web: ^1.0.0 diff --git a/packages/devtools_app/test/screens/performance/queued_microtasks/queued_microtasks_controller_test.dart b/packages/devtools_app/test/screens/performance/queued_microtasks/queued_microtasks_controller_test.dart index 6a904b8871b..87cfe87b860 100644 --- a/packages/devtools_app/test/screens/performance/queued_microtasks/queued_microtasks_controller_test.dart +++ b/packages/devtools_app/test/screens/performance/queued_microtasks/queued_microtasks_controller_test.dart @@ -28,7 +28,11 @@ void main() { }); test('refresh', () async { + expect(controller.status.value, QueuedMicrotasksControllerStatus.empty); + expect(controller.queuedMicrotasks.value, null); + await controller.refresh(); + expect(controller.status.value, QueuedMicrotasksControllerStatus.ready); expect(controller.queuedMicrotasks.value, testQueuedMicrotasks); }); diff --git a/packages/devtools_app/test/screens/performance/tabbed_performance_view_test.dart b/packages/devtools_app/test/screens/performance/tabbed_performance_view_test.dart index de2d634ff10..9ab6787f993 100644 --- a/packages/devtools_app/test/screens/performance/tabbed_performance_view_test.dart +++ b/packages/devtools_app/test/screens/performance/tabbed_performance_view_test.dart @@ -34,7 +34,7 @@ void main() { vmFlags: [ ( flagName: profileMicrotasks, - value: shouldEnableMicrotaskProfiling ? 'true' : 'false', + value: shouldEnableMicrotaskProfiling.toString(), ), ], ), @@ -260,11 +260,14 @@ void main() { final mockQueuedMicrotasksController = controller.queuedMicrotasksController as MockQueuedMicrotasksController; - when(mockQueuedMicrotasksController.status).thenReturn( - const FixedValueListenable( - QueuedMicrotasksControllerStatus.empty, - ), - ); + + final mockQueuedMicrotasksControllerStatus = + ValueNotifier( + QueuedMicrotasksControllerStatus.empty, + ); + when( + mockQueuedMicrotasksController.status, + ).thenReturn(mockQueuedMicrotasksControllerStatus); await pumpView(tester); @@ -284,11 +287,8 @@ void main() { // Then, we verify that details about the queued microtasks are shown // when [controller.queuedMicrotasksController]'s status is ready. - when(mockQueuedMicrotasksController.status).thenReturn( - const FixedValueListenable( - QueuedMicrotasksControllerStatus.ready, - ), - ); + mockQueuedMicrotasksControllerStatus.value = + QueuedMicrotasksControllerStatus.ready; when( mockQueuedMicrotasksController.selectedMicrotask, ).thenReturn(FixedValueListenable(testMicrotask)); @@ -296,13 +296,10 @@ void main() { mockQueuedMicrotasksController.queuedMicrotasks, ).thenReturn(FixedValueListenable(testQueuedMicrotasks)); - await tester.tap(find.text('Timeline Events')); - await tester.pumpAndSettle(); - await tester.tap(find.text('Queued Microtasks')); await tester.pumpAndSettle(); expect(find.byType(QueuedMicrotaskSelector), findsOneWidget); - expect(find.byType(StackTraceView), findsOneWidget); + expect(find.byType(MicrotaskStackTraceView), findsOneWidget); }); }, ); From 55a8ed3e6af2d891421384d108a65ec607e5f354 Mon Sep 17 00:00:00 2001 From: Derek Xu Date: Thu, 12 Jun 2025 13:11:35 -0400 Subject: [PATCH 3/4] Move tab from Performance view to VM Tools screen --- packages/devtools_app/lib/devtools_app.dart | 2 +- .../queued_microtasks_view.dart | 252 ---------------- .../performance/performance_controller.dart | 9 - .../performance/performance_screen.dart | 21 +- .../performance/tabbed_performance_view.dart | 27 -- .../queued_microtasks_controller.dart | 26 +- .../queued_microtasks_view.dart | 271 ++++++++++++++++++ .../vm_developer_tools_screen.dart | 12 + .../lib/src/shared/analytics/constants.dart | 1 + .../constants/_performance_constants.dart | 1 - .../release_notes/NEXT_RELEASE_NOTES.md | 2 +- .../tabbed_performance_view_test.dart | 163 ++--------- .../queued_microtasks_controller_test.dart | 6 +- .../queued_microtasks_view_test.dart | 74 +++++ .../vm_developer/vm_developer_test_utils.dart | 12 + .../scenes/performance/default.dart | 2 +- .../performance/sample_performance_data.dart | 12 - .../lib/src/mocks/generated.dart | 1 - .../src/mocks/generated_mocks_factories.dart | 6 - 19 files changed, 398 insertions(+), 502 deletions(-) delete mode 100644 packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_view.dart rename packages/devtools_app/lib/src/screens/{performance/panes => vm_developer}/queued_microtasks/queued_microtasks_controller.dart (74%) create mode 100644 packages/devtools_app/lib/src/screens/vm_developer/queued_microtasks/queued_microtasks_view.dart rename packages/devtools_app/test/screens/{performance => vm_developer}/queued_microtasks/queued_microtasks_controller_test.dart (86%) create mode 100644 packages/devtools_app/test/screens/vm_developer/queued_microtasks/queued_microtasks_view_test.dart diff --git a/packages/devtools_app/lib/devtools_app.dart b/packages/devtools_app/lib/devtools_app.dart index d68416fba79..ddfa7466ab2 100644 --- a/packages/devtools_app/lib/devtools_app.dart +++ b/packages/devtools_app/lib/devtools_app.dart @@ -41,7 +41,6 @@ export 'src/screens/performance/panes/flutter_frames/flutter_frame_model.dart'; export 'src/screens/performance/panes/flutter_frames/flutter_frames_chart.dart'; export 'src/screens/performance/panes/flutter_frames/flutter_frames_controller.dart'; export 'src/screens/performance/panes/frame_analysis/frame_analysis_model.dart'; -export 'src/screens/performance/panes/queued_microtasks/queued_microtasks_controller.dart'; export 'src/screens/performance/panes/rebuild_stats/rebuild_stats_controller.dart'; export 'src/screens/performance/panes/rebuild_stats/rebuild_stats_model.dart'; export 'src/screens/performance/panes/timeline_events/perfetto/tracing/model.dart'; @@ -58,6 +57,7 @@ export 'src/screens/vm_developer/object_inspector/class_hierarchy_explorer.dart' export 'src/screens/vm_developer/object_inspector/class_hierarchy_explorer_controller.dart'; export 'src/screens/vm_developer/object_inspector/object_inspector_view_controller.dart'; export 'src/screens/vm_developer/object_inspector/vm_object_model.dart'; +export 'src/screens/vm_developer/queued_microtasks/queued_microtasks_controller.dart'; export 'src/screens/vm_developer/vm_developer_tools_controller.dart'; export 'src/screens/vm_developer/vm_developer_tools_screen.dart'; export 'src/screens/vm_developer/vm_service_private_extensions.dart'; diff --git a/packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_view.dart b/packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_view.dart deleted file mode 100644 index ed3d1277b18..00000000000 --- a/packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_view.dart +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright 2025 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'package:collection/collection.dart' show ListExtensions; -import 'package:devtools_app_shared/ui.dart'; -import 'package:devtools_app_shared/utils.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart' show DateFormat; -import 'package:vm_service/vm_service.dart'; - -import '../../../../shared/analytics/constants.dart' as gac; -import '../../../../shared/primitives/utils.dart' show SortDirection; -import '../../../../shared/table/table.dart' show FlatTable; -import '../../../../shared/table/table_data.dart'; -import '../../../../shared/ui/common_widgets.dart' - show CenteredMessage, RefreshButton; -import 'queued_microtasks_controller.dart'; - -class RefreshQueuedMicrotasksButton extends StatelessWidget { - const RefreshQueuedMicrotasksButton({super.key, required this.controller}); - - final QueuedMicrotasksController controller; - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: controller.status, - builder: (_, status, _) { - return RefreshButton( - iconOnly: true, - outlined: false, - onPressed: status == QueuedMicrotasksControllerStatus.refreshing - ? null - : controller.refresh, - tooltip: - "Take a new snapshot of the selected isolate's microtask queue.", - gaScreen: gac.performance, - gaSelection: gac.PerformanceEvents.refreshQueuedMicrotasks.name, - ); - }, - ); - } -} - -class RefreshQueuedMicrotasksInstructions extends StatelessWidget { - const RefreshQueuedMicrotasksInstructions({super.key}); - - @override - Widget build(BuildContext context) { - return Center( - child: RichText( - text: TextSpan( - style: Theme.of(context).regularTextStyle, - children: [ - const TextSpan(text: 'Click the refresh button '), - WidgetSpan(child: Icon(Icons.refresh, size: defaultIconSize)), - const TextSpan( - text: - " to take a new snapshot of the selected isolate's " - 'microtask queue.', - ), - ], - ), - ), - ); - } -} - -/// In the response returned by the VM Service, microtasks are sorted in -/// ascending order of when they will be dequeued, i.e. the microtask that will -/// run earliest is at index 0 of the returned list. We use those indices of the -/// returned list to sort the entries of the microtask selector, so that they -/// they also appear in ascending order of when they will be dequeued. -typedef IndexedMicrotask = ({int index, Microtask microtask}); - -class _MicrotaskIdColumn extends ColumnData { - _MicrotaskIdColumn() - : super.wide('Microtask ID', alignment: ColumnAlignment.center); - - @override - int getValue(IndexedMicrotask indexedMicrotask) => indexedMicrotask.index; - - @override - String getDisplayValue(IndexedMicrotask indexedMicrotask) => - indexedMicrotask.microtask.id!.toString(); -} - -class QueuedMicrotaskSelector extends StatelessWidget { - const QueuedMicrotaskSelector({ - super.key, - required List indexedMicrotasks, - required void Function(Microtask?) onMicrotaskSelected, - }) : _indexedMicrotasks = indexedMicrotasks, - _setSelectedMicrotask = onMicrotaskSelected; - - static final _idColumn = _MicrotaskIdColumn(); - final List _indexedMicrotasks; - final void Function(Microtask?) _setSelectedMicrotask; - - @override - Widget build(BuildContext context) => FlatTable( - keyFactory: (IndexedMicrotask microtask) => ValueKey(microtask.index), - data: _indexedMicrotasks, - dataKey: 'queued-microtask-selector', - columns: [_idColumn], - defaultSortColumn: _idColumn, - defaultSortDirection: SortDirection.ascending, - onItemSelected: (indexedMicrotask) => - _setSelectedMicrotask(indexedMicrotask?.microtask), - ); -} - -class MicrotaskStackTraceView extends StatelessWidget { - const MicrotaskStackTraceView({super.key, required selectedMicrotask}) - : _selectedMicrotask = selectedMicrotask; - - final Microtask? _selectedMicrotask; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Column( - children: [ - Container( - height: defaultHeaderHeight, - padding: const EdgeInsets.only(left: defaultSpacing), - alignment: Alignment.centerLeft, - child: OutlineDecoration.onlyBottom( - child: const Row( - children: [ - Text('Stack trace captured when microtask was enqueued'), - ], - ), - ), - ), - Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: denseRowSpacing, - horizontal: defaultSpacing, - ), - child: SelectableText( - style: theme.fixedFontStyle, - _selectedMicrotask!.stackTrace.toString(), - ), - ), - ), - ], - ), - ], - ); - } -} - -class QueuedMicrotasksTabView extends StatefulWidget { - const QueuedMicrotasksTabView({super.key, required this.controller}); - - final QueuedMicrotasksController controller; - - @override - State createState() => - _QueuedMicrotasksTabViewState(); -} - -class _QueuedMicrotasksTabViewState extends State - with AutoDisposeMixin { - static final _dateTimeFormat = DateFormat('HH:mm:ss.SSS (MM/dd/yy)'); - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: widget.controller.status, - builder: (context, status, _) { - if (status == QueuedMicrotasksControllerStatus.empty) { - return const RefreshQueuedMicrotasksInstructions(); - } else if (status == QueuedMicrotasksControllerStatus.refreshing) { - return const CenteredMessage(message: 'Refreshing...'); - } else { - return ValueListenableBuilder( - valueListenable: widget.controller.queuedMicrotasks, - builder: (_, queuedMicrotasks, _) { - assert(queuedMicrotasks != null); - if (queuedMicrotasks == null) { - return const CenteredMessage(message: 'Unexpected null value'); - } - - final indexedMicrotasks = queuedMicrotasks.microtasks! - .mapIndexed( - (index, microtask) => (index: index, microtask: microtask), - ) - .toList(); - final formattedTimestamp = _dateTimeFormat.format( - DateTime.fromMicrosecondsSinceEpoch( - queuedMicrotasks.timestamp!, - ), - ); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric( - vertical: denseRowSpacing, - horizontal: defaultSpacing, - ), - child: Text( - 'Viewing snapshot that was taken at $formattedTimestamp.', - ), - ), - Expanded( - child: SplitPane( - axis: Axis.horizontal, - initialFractions: const [0.15, 0.85], - children: [ - OutlineDecoration( - child: QueuedMicrotaskSelector( - indexedMicrotasks: indexedMicrotasks, - onMicrotaskSelected: - widget.controller.setSelectedMicrotask, - ), - ), - ValueListenableBuilder( - valueListenable: widget.controller.selectedMicrotask, - builder: (_, selectedMicrotask, _) => - OutlineDecoration( - child: selectedMicrotask == null - ? const CenteredMessage( - message: - 'Select a microtask ID on the left ' - 'to see information about the ' - 'corresponding microtask.', - ) - : MicrotaskStackTraceView( - selectedMicrotask: selectedMicrotask, - ), - ), - ), - ], - ), - ), - ], - ); - }, - ); - } - }, - ); - } -} 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 914ba111ba4..cfb4469afca 100644 --- a/packages/devtools_app/lib/src/screens/performance/performance_controller.dart +++ b/packages/devtools_app/lib/src/screens/performance/performance_controller.dart @@ -22,7 +22,6 @@ import '../../shared/offline/offline_data.dart'; import 'panes/controls/enhance_tracing/enhance_tracing_controller.dart'; import 'panes/flutter_frames/flutter_frame_model.dart'; import 'panes/flutter_frames/flutter_frames_controller.dart'; -import 'panes/queued_microtasks/queued_microtasks_controller.dart'; import 'panes/rebuild_stats/rebuild_stats_controller.dart'; import 'panes/rebuild_stats/rebuild_stats_model.dart'; import 'panes/timeline_events/timeline_events_controller.dart'; @@ -54,8 +53,6 @@ class PerformanceController extends DevToolsScreenController late final TimelineEventsController timelineEventsController; - late final QueuedMicrotasksController queuedMicrotasksController; - late final RebuildStatsController rebuildStatsController; late List _featureControllers; @@ -93,8 +90,6 @@ class PerformanceController extends DevToolsScreenController Future get initialized => _initialized.future; final _initialized = Completer(); - final isQueuedMicrotasksFeatureActive = ValueNotifier(false); - @override void init() { super.init(); @@ -102,12 +97,10 @@ class PerformanceController extends DevToolsScreenController // only create a controller when it is needed, flutterFramesController = FlutterFramesController(this); timelineEventsController = TimelineEventsController(this); - queuedMicrotasksController = QueuedMicrotasksController(this); rebuildStatsController = RebuildStatsController(this); _featureControllers = [ flutterFramesController, timelineEventsController, - queuedMicrotasksController, rebuildStatsController, ]; @@ -258,8 +251,6 @@ class PerformanceController extends DevToolsScreenController featureController != null && c == featureController, ), ); - isQueuedMicrotasksFeatureActive.value = - queuedMicrotasksController.isActiveFeature; } /// Clears the timeline data currently stored by the controller as well the 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 6780f97ffbf..ac10baf610d 100644 --- a/packages/devtools_app/lib/src/screens/performance/performance_screen.dart +++ b/packages/devtools_app/lib/src/screens/performance/performance_screen.dart @@ -6,7 +6,6 @@ import 'dart:async'; import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; -import 'package:flutter/foundation.dart' show ValueListenable; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:provider/provider.dart'; @@ -36,18 +35,13 @@ class PerformanceScreen extends Screen { @override String get docPageId => id; - final _showIsolateSelector = ValueNotifier(false); - - @override - ValueListenable get showIsolateSelector => _showIsolateSelector; - @override Widget buildScreenBody(BuildContext context) { if (serviceConnection.serviceManager.connectedApp?.isDartWebAppNow ?? false) { return const WebPerformanceScreenBody(); } - return PerformanceScreenBody(showIsolateSelector: _showIsolateSelector); + return const PerformanceScreenBody(); } @override @@ -57,14 +51,7 @@ class PerformanceScreen extends Screen { } class PerformanceScreenBody extends StatefulWidget { - const PerformanceScreenBody({ - super.key, - // This allows the body to modify the value that gets returned by the - // enclosing [PerformanceScreen]'s `showIsolateSelector` [ValueListenable]. - required this.showIsolateSelector, - }); - - final ValueNotifier showIsolateSelector; + const PerformanceScreenBody({super.key}); @override PerformanceScreenBodyState createState() => PerformanceScreenBodyState(); @@ -81,10 +68,6 @@ class PerformanceScreenBodyState extends State controller = screenControllers.lookup(); addAutoDisposeListener(offlineDataController.showingOfflineData); addAutoDisposeListener(controller.loadingOfflineData); - addAutoDisposeListener(controller.isQueuedMicrotasksFeatureActive, () { - widget.showIsolateSelector.value = - controller.isQueuedMicrotasksFeatureActive.value; - }); } @override 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 6eeb3ce33d4..05eef54cd1c 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 @@ -8,7 +8,6 @@ import 'package:collection/collection.dart'; import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/material.dart'; -import '../../service/vm_flags.dart' as vm_flags; import '../../shared/analytics/constants.dart' as gac; import '../../shared/globals.dart'; import '../../shared/ui/common_widgets.dart'; @@ -16,7 +15,6 @@ import '../../shared/ui/tab.dart'; import 'panes/flutter_frames/flutter_frame_model.dart'; import 'panes/flutter_frames/flutter_frames_controller.dart'; import 'panes/frame_analysis/frame_analysis.dart'; -import 'panes/queued_microtasks/queued_microtasks_view.dart'; import 'panes/rebuild_stats/rebuild_stats.dart'; import 'panes/timeline_events/timeline_events_view.dart'; import 'performance_controller.dart'; @@ -71,20 +69,9 @@ class _TabbedPerformanceViewState extends State offlineData.rebuildCountModel != null; } - // The value of the `--profile-microtasks` VM flag cannot be modified once - // the VM has started running. - final showQueuedMicrotasks = - !isOffline && - serviceConnection.vmFlagManager - .flag(vm_flags.profileMicrotasks) - ?.value - .valueAsString == - 'true'; - final tabsAndControllers = _generateTabs( showFrameAnalysis: showFrameAnalysis, showRebuildStats: showRebuildStats, - showQueuedMicrotasks: showQueuedMicrotasks, ); final tabs = tabsAndControllers .map((t) => (tab: t.tab, tabView: t.tabView)) @@ -129,7 +116,6 @@ class _TabbedPerformanceViewState extends State _generateTabs({ required bool showFrameAnalysis, required bool showRebuildStats, - required bool showQueuedMicrotasks, }) { if (showFrameAnalysis || showRebuildStats) { assert(serviceConnection.serviceManager.connectedApp!.isFlutterAppNow!); @@ -177,19 +163,6 @@ class _TabbedPerformanceViewState extends State ), featureController: controller.timelineEventsController, ), - if (showQueuedMicrotasks) - ( - tab: _buildTab( - tabName: 'Queued Microtasks', - trailing: RefreshQueuedMicrotasksButton( - controller: controller.queuedMicrotasksController, - ), - ), - tabView: QueuedMicrotasksTabView( - controller: controller.queuedMicrotasksController, - ), - featureController: controller.queuedMicrotasksController, - ), ]; } diff --git a/packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_controller.dart b/packages/devtools_app/lib/src/screens/vm_developer/queued_microtasks/queued_microtasks_controller.dart similarity index 74% rename from packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_controller.dart rename to packages/devtools_app/lib/src/screens/vm_developer/queued_microtasks/queued_microtasks_controller.dart index 58129c9c636..e6779886612 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/queued_microtasks/queued_microtasks_controller.dart +++ b/packages/devtools_app/lib/src/screens/vm_developer/queued_microtasks/queued_microtasks_controller.dart @@ -8,17 +8,14 @@ import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/foundation.dart'; import 'package:vm_service/vm_service.dart'; -import '../../../../shared/globals.dart'; -import '../../../../shared/utils/future_work_tracker.dart'; -import '../../performance_controller.dart'; -import '../../performance_model.dart'; -import '../flutter_frames/flutter_frame_model.dart'; +import '../../../shared/globals.dart'; +import '../../../shared/utils/future_work_tracker.dart'; enum QueuedMicrotasksControllerStatus { empty, refreshing, ready } -class QueuedMicrotasksController extends PerformanceFeatureController +class QueuedMicrotasksController extends DisposableController with AutoDisposeControllerMixin { - QueuedMicrotasksController(super.controller) { + QueuedMicrotasksController() { addAutoDisposeListener(_refreshWorkTracker.active, () { final active = _refreshWorkTracker.active.value; if (active) { @@ -42,9 +39,6 @@ class QueuedMicrotasksController extends PerformanceFeatureController final _refreshWorkTracker = FutureWorkTracker(); - @override - void onBecomingActive() {} - Future refresh() => _refreshWorkTracker.track(() async { _selectedMicrotask.value = null; @@ -65,18 +59,6 @@ class QueuedMicrotasksController extends PerformanceFeatureController _selectedMicrotask.value = microtask; } - @override - Future handleSelectedFrame(FlutterFrame frame) async {} - - @override - Future setOfflineData(OfflinePerformanceData offlineData) async {} - - @override - Future clearData({bool partial = false}) async { - _selectedMicrotask.value = null; - _queuedMicrotasks.value = null; - } - @override void dispose() { _status.dispose(); diff --git a/packages/devtools_app/lib/src/screens/vm_developer/queued_microtasks/queued_microtasks_view.dart b/packages/devtools_app/lib/src/screens/vm_developer/queued_microtasks/queued_microtasks_view.dart new file mode 100644 index 00000000000..1f177b8239e --- /dev/null +++ b/packages/devtools_app/lib/src/screens/vm_developer/queued_microtasks/queued_microtasks_view.dart @@ -0,0 +1,271 @@ +// Copyright 2025 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +import 'package:collection/collection.dart' show ListExtensions; +import 'package:devtools_app_shared/ui.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart' show DateFormat; +import 'package:vm_service/vm_service.dart'; + +import '../../../shared/analytics/constants.dart' as gac; +import '../../../shared/primitives/utils.dart' show SortDirection; +import '../../../shared/table/table.dart' show FlatTable; +import '../../../shared/table/table_data.dart'; +import '../../../shared/ui/common_widgets.dart'; +import '../vm_developer_tools_screen.dart'; +import 'queued_microtasks_controller.dart'; + +class RefreshQueuedMicrotasksButton extends StatelessWidget { + const RefreshQueuedMicrotasksButton({super.key, required this.controller}); + + final QueuedMicrotasksController controller; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller.status, + builder: (_, status, _) { + return RefreshButton( + onPressed: status == QueuedMicrotasksControllerStatus.refreshing + ? null + : controller.refresh, + tooltip: + "Take a new snapshot of the selected isolate's microtask queue.", + gaScreen: gac.vmTools, + gaSelection: gac.refreshQueuedMicrotasks, + ); + }, + ); + } +} + +class RefreshQueuedMicrotasksInstructions extends StatelessWidget { + const RefreshQueuedMicrotasksInstructions({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: RichText( + text: TextSpan( + style: Theme.of(context).regularTextStyle, + children: [ + const TextSpan(text: 'Click the refresh button '), + WidgetSpan(child: Icon(Icons.refresh, size: defaultIconSize)), + const TextSpan( + text: + " to take a new snapshot of the selected isolate's " + 'microtask queue.', + ), + ], + ), + ), + ); + } +} + +/// In the response returned by the VM Service, microtasks are sorted in +/// ascending order of when they will be dequeued, i.e. the microtask that will +/// run earliest is at index 0 of the returned list. We use those indices of the +/// returned list to sort the entries of the microtask selector, so that they +/// they also appear in ascending order of when they will be dequeued. +typedef IndexedMicrotask = ({int index, Microtask microtask}); + +class _MicrotaskIdColumn extends ColumnData { + _MicrotaskIdColumn() + : super.wide('Microtask ID', alignment: ColumnAlignment.center); + + @override + int getValue(IndexedMicrotask indexedMicrotask) => indexedMicrotask.index; + + @override + String getDisplayValue(IndexedMicrotask indexedMicrotask) => + indexedMicrotask.microtask.id!.toString(); +} + +class QueuedMicrotaskSelector extends StatelessWidget { + const QueuedMicrotaskSelector({ + super.key, + required List indexedMicrotasks, + required void Function(Microtask?) onMicrotaskSelected, + }) : _indexedMicrotasks = indexedMicrotasks, + _setSelectedMicrotask = onMicrotaskSelected; + + static final _idColumn = _MicrotaskIdColumn(); + final List _indexedMicrotasks; + final void Function(Microtask?) _setSelectedMicrotask; + + @override + Widget build(BuildContext context) => FlatTable( + keyFactory: (IndexedMicrotask microtask) => ValueKey(microtask.index), + data: _indexedMicrotasks, + dataKey: 'queued-microtask-selector', + columns: [_idColumn], + defaultSortColumn: _idColumn, + defaultSortDirection: SortDirection.ascending, + onItemSelected: (indexedMicrotask) => + _setSelectedMicrotask(indexedMicrotask?.microtask), + ); +} + +class MicrotaskStackTraceView extends StatelessWidget { + const MicrotaskStackTraceView({super.key, required selectedMicrotask}) + : _selectedMicrotask = selectedMicrotask; + + final Microtask? _selectedMicrotask; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + children: [ + Container( + height: defaultHeaderHeight, + padding: const EdgeInsets.only(left: defaultSpacing), + alignment: Alignment.centerLeft, + child: OutlineDecoration.onlyBottom( + child: const Row( + children: [ + Text('Stack trace captured when microtask was enqueued'), + ], + ), + ), + ), + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: denseRowSpacing, + horizontal: defaultSpacing, + ), + child: SelectableText( + style: theme.fixedFontStyle, + _selectedMicrotask!.stackTrace.toString(), + ), + ), + ), + ], + ), + ], + ); + } +} + +class QueuedMicrotasksView extends VMDeveloperView { + const QueuedMicrotasksView() + : super(title: 'Queued Microtasks', icon: Icons.pending_actions); + + @override + bool get showIsolateSelector => true; + + @override + Widget build(BuildContext context) => QueuedMicrotasksViewBody(); +} + +class QueuedMicrotasksViewBody extends StatelessWidget { + QueuedMicrotasksViewBody({super.key}); + + @visibleForTesting + static final dateTimeFormat = DateFormat('HH:mm:ss.SSS (MM/dd/yy)'); + final controller = QueuedMicrotasksController(); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RefreshQueuedMicrotasksButton(controller: controller), + const SizedBox(height: denseRowSpacing), + Expanded( + child: OutlineDecoration( + child: ValueListenableBuilder( + valueListenable: controller.status, + builder: (context, status, _) { + if (status == QueuedMicrotasksControllerStatus.empty) { + return const RefreshQueuedMicrotasksInstructions(); + } else if (status == + QueuedMicrotasksControllerStatus.refreshing) { + return const CenteredMessage(message: 'Refreshing...'); + } else { + return ValueListenableBuilder( + valueListenable: controller.queuedMicrotasks, + builder: (_, queuedMicrotasks, _) { + assert(queuedMicrotasks != null); + if (queuedMicrotasks == null) { + return const CenteredMessage( + message: 'Unexpected null value', + ); + } + + final indexedMicrotasks = queuedMicrotasks.microtasks! + .mapIndexed( + (index, microtask) => + (index: index, microtask: microtask), + ) + .toList(); + final formattedTimestamp = dateTimeFormat.format( + DateTime.fromMicrosecondsSinceEpoch( + queuedMicrotasks.timestamp!, + ), + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: denseRowSpacing, + horizontal: defaultSpacing, + ), + child: Text( + 'Viewing snapshot that was taken at ' + '$formattedTimestamp.', + ), + ), + Expanded( + child: SplitPane( + axis: Axis.horizontal, + initialFractions: const [0.15, 0.85], + children: [ + OutlineDecoration( + child: QueuedMicrotaskSelector( + indexedMicrotasks: indexedMicrotasks, + onMicrotaskSelected: + controller.setSelectedMicrotask, + ), + ), + ValueListenableBuilder( + valueListenable: controller.selectedMicrotask, + builder: (_, selectedMicrotask, _) => + OutlineDecoration( + child: selectedMicrotask == null + ? const CenteredMessage( + message: + 'Select a microtask ID on ' + 'the left to see ' + 'information about the ' + 'corresponding microtask.', + ) + : MicrotaskStackTraceView( + selectedMicrotask: + selectedMicrotask, + ), + ), + ), + ], + ), + ), + ], + ); + }, + ); + } + }, + ), + ), + ), + ], + ); + } +} diff --git a/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_tools_screen.dart b/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_tools_screen.dart index f7564d5b638..e30f8cdae62 100644 --- a/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_tools_screen.dart +++ b/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_tools_screen.dart @@ -6,11 +6,13 @@ import 'package:devtools_app_shared/ui.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import '../../service/vm_flags.dart' as vm_flags; import '../../shared/framework/screen.dart'; import '../../shared/globals.dart'; import 'isolate_statistics/isolate_statistics_view.dart'; import 'object_inspector/object_inspector_view.dart'; import 'process_memory/process_memory_view.dart'; +import 'queued_microtasks/queued_microtasks_view.dart'; import 'vm_developer_tools_controller.dart'; import 'vm_statistics/vm_statistics_view.dart'; @@ -49,11 +51,21 @@ class VMDeveloperToolsScreen extends Screen { class VMDeveloperToolsScreenBody extends StatelessWidget { const VMDeveloperToolsScreenBody({super.key}); + // The value of the `--profile-microtasks` VM flag cannot be modified once + // the VM has started running. + static final showQueuedMicrotasks = + serviceConnection.vmFlagManager + .flag(vm_flags.profileMicrotasks) + ?.value + .valueAsString == + 'true'; + static final views = [ const VMStatisticsView(), const IsolateStatisticsView(), ObjectInspectorView(), const VMProcessMemoryView(), + if (showQueuedMicrotasks) const QueuedMicrotasksView(), ]; @override diff --git a/packages/devtools_app/lib/src/shared/analytics/constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants.dart index ad42fcfec24..2e9ec0da4df 100644 --- a/packages/devtools_app/lib/src/shared/analytics/constants.dart +++ b/packages/devtools_app/lib/src/shared/analytics/constants.dart @@ -115,6 +115,7 @@ const refreshIsolateStatistics = 'refreshIsolateStatistics'; const refreshVmStatistics = 'refreshVmStatistics'; const refreshProcessMemoryStatistics = 'refreshProcessMemoryStatistics'; const requestSize = 'requestSize'; +const refreshQueuedMicrotasks = 'refreshQueuedMicrotasks'; // Settings actions: const settingsDialog = 'settings'; diff --git a/packages/devtools_app/lib/src/shared/analytics/constants/_performance_constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants/_performance_constants.dart index 2982704d395..1b548e82749 100644 --- a/packages/devtools_app/lib/src/shared/analytics/constants/_performance_constants.dart +++ b/packages/devtools_app/lib/src/shared/analytics/constants/_performance_constants.dart @@ -7,7 +7,6 @@ part of '../constants.dart'; enum PerformanceEvents { refreshTimelineEvents, includeCpuSamplesInTimeline, - refreshQueuedMicrotasks, performanceOverlay, framesChartVisibility, selectFlutterFrame, diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 8e6a768cf48..b4138e50df8 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -23,7 +23,7 @@ TODO: Remove this section if there are not any general updates. ## Performance updates -- Added a Queued Microtasks tab to the Performance View, which allows a user to +- Added a Queued Microtasks tab to the VM Tools screen, which allows a user to see details about the microtasks scheduled in an isolate's microtask queue. This tab currently only appears when DevTools is connected to a Flutter or Dart app started with `--profile-microtasks`. - diff --git a/packages/devtools_app/test/screens/performance/tabbed_performance_view_test.dart b/packages/devtools_app/test/screens/performance/tabbed_performance_view_test.dart index 9ab6787f993..66239af683d 100644 --- a/packages/devtools_app/test/screens/performance/tabbed_performance_view_test.dart +++ b/packages/devtools_app/test/screens/performance/tabbed_performance_view_test.dart @@ -4,7 +4,6 @@ import 'package:devtools_app/devtools_app.dart'; import 'package:devtools_app/src/screens/performance/panes/frame_analysis/frame_analysis.dart'; -import 'package:devtools_app/src/screens/performance/panes/queued_microtasks/queued_microtasks_view.dart'; import 'package:devtools_app/src/screens/performance/panes/rebuild_stats/rebuild_stats.dart'; import 'package:devtools_app/src/screens/performance/panes/timeline_events/perfetto/perfetto.dart'; import 'package:devtools_app/src/screens/performance/panes/timeline_events/timeline_events_view.dart'; @@ -25,22 +24,12 @@ void main() { late FakeServiceConnectionManager fakeServiceConnection; late MockPerformanceController controller; - Future setUpServiceManagerWithTimeline({ - bool shouldEnableMicrotaskProfiling = false, - }) async { + Future setUpServiceManagerWithTimeline() async { fakeServiceConnection = FakeServiceConnectionManager( service: FakeServiceManager.createFakeService( timelineData: perfettoVmTimeline, - vmFlags: [ - ( - flagName: profileMicrotasks, - value: shouldEnableMicrotaskProfiling.toString(), - ), - ], ), ); - await fakeServiceConnection.serviceManager.flagsInitialized.future; - final app = fakeServiceConnection.serviceManager.connectedApp!; mockConnectedApp( app, @@ -62,7 +51,8 @@ void main() { } group('TabbedPerformanceView', () { - setUp(() { + setUp(() async { + await setUpServiceManagerWithTimeline(); setGlobal( DevToolsEnvironmentParameters, ExternalDevToolsEnvironmentParameters(), @@ -115,47 +105,21 @@ void main() { const windowSize = Size(2225.0, 1000.0); - testWidgetsWithWindowSize( - 'builds content successfully when connected to a Flutter app and ' - 'microtask profiling is disabled', - windowSize, - (WidgetTester tester) async { - await tester.runAsync(() async { - await setUpServiceManagerWithTimeline(); - await pumpView(tester); + testWidgetsWithWindowSize('builds content successfully', windowSize, ( + WidgetTester tester, + ) async { + await tester.runAsync(() async { + await setUpServiceManagerWithTimeline(); + await pumpView(tester); - expect(find.byType(AnalyticsTabbedView), findsOneWidget); - expect(find.byType(DevToolsTab), findsNWidgets(3)); - - expect(find.text('Timeline Events'), findsOneWidget); - expect(find.text('Frame Analysis'), findsOneWidget); - expect(find.text('Rebuild Stats'), findsOneWidget); - expect(find.text('Queued Microtasks'), findsNothing); - }); - }, - ); - - testWidgetsWithWindowSize( - 'builds content successfully when connected to a Flutter app and ' - 'microtask profiling is enabled', - windowSize, - (WidgetTester tester) async { - await tester.runAsync(() async { - await setUpServiceManagerWithTimeline( - shouldEnableMicrotaskProfiling: true, - ); - await pumpView(tester); + expect(find.byType(AnalyticsTabbedView), findsOneWidget); + expect(find.byType(DevToolsTab), findsNWidgets(3)); - expect(find.byType(AnalyticsTabbedView), findsOneWidget); - expect(find.byType(DevToolsTab), findsNWidgets(4)); - - expect(find.text('Frame Analysis'), findsOneWidget); - expect(find.text('Rebuild Stats'), findsOneWidget); - expect(find.text('Timeline Events'), findsOneWidget); - expect(find.text('Queued Microtasks'), findsOneWidget); - }); - }, - ); + expect(find.text('Timeline Events'), findsOneWidget); + expect(find.text('Frame Analysis'), findsOneWidget); + expect(find.text('Rebuild Stats'), findsOneWidget); + }); + }); testWidgetsWithWindowSize( 'builds content for Frame Analysis tab with selected frame', @@ -244,69 +208,7 @@ void main() { ); testWidgetsWithWindowSize( - 'builds content for Queued Microtasks tab when microtask profiling is ' - 'enabled', - windowSize, - (WidgetTester tester) async { - await tester.runAsync(() async { - await setUpServiceManagerWithTimeline( - shouldEnableMicrotaskProfiling: true, - ); - - // First, we verify that instructions are shown in the Queued - // Microtasks tab when [controller.queuedMicrotasksController]'s - // status is empty. - - final mockQueuedMicrotasksController = - controller.queuedMicrotasksController - as MockQueuedMicrotasksController; - - final mockQueuedMicrotasksControllerStatus = - ValueNotifier( - QueuedMicrotasksControllerStatus.empty, - ); - when( - mockQueuedMicrotasksController.status, - ).thenReturn(mockQueuedMicrotasksControllerStatus); - - await pumpView(tester); - - expect(find.byType(AnalyticsTabbedView), findsOneWidget); - expect(find.byType(DevToolsTab), findsNWidgets(4)); - - await tester.tap(find.text('Queued Microtasks')); - await tester.pumpAndSettle(); - - expect(find.byType(RefreshQueuedMicrotasksButton), findsOneWidget); - expect(find.byType(QueuedMicrotasksTabView), findsOneWidget); - expect( - find.byType(RefreshQueuedMicrotasksInstructions), - findsOneWidget, - ); - - // Then, we verify that details about the queued microtasks are shown - // when [controller.queuedMicrotasksController]'s status is ready. - - mockQueuedMicrotasksControllerStatus.value = - QueuedMicrotasksControllerStatus.ready; - when( - mockQueuedMicrotasksController.selectedMicrotask, - ).thenReturn(FixedValueListenable(testMicrotask)); - when( - mockQueuedMicrotasksController.queuedMicrotasks, - ).thenReturn(FixedValueListenable(testQueuedMicrotasks)); - - await tester.pumpAndSettle(); - - expect(find.byType(QueuedMicrotaskSelector), findsOneWidget); - expect(find.byType(MicrotaskStackTraceView), findsOneWidget); - }); - }, - ); - - testWidgetsWithWindowSize( - 'only shows Timeline Events tab when connected to a non-Flutter app and ' - 'microtask profiling is disabled', + 'only shows Timeline Events tab for non-flutter app', windowSize, (WidgetTester tester) async { await tester.runAsync(() async { @@ -325,37 +227,6 @@ void main() { expect(find.byType(AnalyticsTabbedView), findsOneWidget); expect(find.byType(DevToolsTab), findsOneWidget); expect(find.text('Timeline Events'), findsOneWidget); - expect(find.text('Queued Microtasks'), findsNothing); - expect(find.text('Frame Analysis'), findsNothing); - expect(find.text('Rebuild Stats'), findsNothing); - }); - }, - ); - - testWidgetsWithWindowSize( - 'only shows Timeline Events and Queued Microtasks tabs when connected to ' - 'a non-flutter app and microtask profiling is enabled', - windowSize, - (WidgetTester tester) async { - await tester.runAsync(() async { - await setUpServiceManagerWithTimeline( - shouldEnableMicrotaskProfiling: true, - ); - final app = fakeServiceConnection.serviceManager.connectedApp!; - mockConnectedApp( - app, - isFlutterApp: false, - isProfileBuild: false, - isWebApp: false, - ); - when(app.flutterVersionNow).thenReturn(null); - - await pumpView(tester); - await tester.pumpAndSettle(); - expect(find.byType(AnalyticsTabbedView), findsOneWidget); - expect(find.byType(DevToolsTab), findsNWidgets(2)); - expect(find.text('Timeline Events'), findsOneWidget); - expect(find.text('Queued Microtasks'), findsOneWidget); expect(find.text('Frame Analysis'), findsNothing); expect(find.text('Rebuild Stats'), findsNothing); }); diff --git a/packages/devtools_app/test/screens/performance/queued_microtasks/queued_microtasks_controller_test.dart b/packages/devtools_app/test/screens/vm_developer/queued_microtasks/queued_microtasks_controller_test.dart similarity index 86% rename from packages/devtools_app/test/screens/performance/queued_microtasks/queued_microtasks_controller_test.dart rename to packages/devtools_app/test/screens/vm_developer/queued_microtasks/queued_microtasks_controller_test.dart index 87cfe87b860..9e7e76cdc17 100644 --- a/packages/devtools_app/test/screens/performance/queued_microtasks/queued_microtasks_controller_test.dart +++ b/packages/devtools_app/test/screens/vm_developer/queued_microtasks/queued_microtasks_controller_test.dart @@ -7,7 +7,7 @@ import 'package:devtools_app_shared/utils.dart'; import 'package:devtools_test/devtools_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../../../test_infra/test_data/performance/sample_performance_data.dart'; +import '../vm_developer_test_utils.dart'; void main() { late FakeServiceConnectionManager fakeServiceConnection; @@ -22,9 +22,7 @@ void main() { ); setGlobal(ServiceConnectionManager, fakeServiceConnection); - controller = QueuedMicrotasksController( - createMockPerformanceControllerWithDefaults(), - ); + controller = QueuedMicrotasksController(); }); test('refresh', () async { diff --git a/packages/devtools_app/test/screens/vm_developer/queued_microtasks/queued_microtasks_view_test.dart b/packages/devtools_app/test/screens/vm_developer/queued_microtasks/queued_microtasks_view_test.dart new file mode 100644 index 00000000000..f024b87f538 --- /dev/null +++ b/packages/devtools_app/test/screens/vm_developer/queued_microtasks/queued_microtasks_view_test.dart @@ -0,0 +1,74 @@ +// Copyright 2025 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +import 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_app/src/screens/vm_developer/queued_microtasks/queued_microtasks_view.dart'; +import 'package:devtools_app_shared/ui.dart'; +import 'package:devtools_app_shared/utils.dart'; +import 'package:devtools_test/devtools_test.dart'; +import 'package:devtools_test/helpers.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../vm_developer_test_utils.dart'; + +void main() { + group('QueuedMicrotasksViewBody', () { + setUp(() { + final fakeServiceConnection = FakeServiceConnectionManager( + service: FakeServiceManager.createFakeService( + queuedMicrotasks: testQueuedMicrotasks, + ), + ); + + setGlobal(ServiceConnectionManager, fakeServiceConnection); + setGlobal(IdeTheme, IdeTheme()); + }); + + const windowSize = Size(2225.0, 1000.0); + + testWidgetsWithWindowSize('interactions work as intended', windowSize, ( + WidgetTester tester, + ) async { + // First, we verify that instructions explaining how to take a snapshot + // are shown in the Queued Microtasks View in its initial state. + + await tester.pumpWidget(wrapSimple(QueuedMicrotasksViewBody())); + + expect(find.byType(RefreshQueuedMicrotasksButton), findsOneWidget); + expect(find.byType(RefreshQueuedMicrotasksInstructions), findsOneWidget); + + // Then, we verify that after taking a snapshot, the user is instructed to + // select a microtask ID to see information about the corresponding + // microtask. + + await tester.tap(find.byType(RefreshQueuedMicrotasksButton)); + await tester.pumpAndSettle(); + + final formattedTimestamp = QueuedMicrotasksViewBody.dateTimeFormat.format( + DateTime.fromMicrosecondsSinceEpoch(testQueuedMicrotasks!.timestamp!), + ); + expect( + find.text('Viewing snapshot that was taken at $formattedTimestamp.'), + findsOneWidget, + ); + expect(find.byType(QueuedMicrotaskSelector), findsOneWidget); + expect( + find.text( + 'Select a microtask ID on the left to see information about the ' + 'corresponding microtask.', + ), + findsOneWidget, + ); + + // Finally, we verify that after selecting a microtask ID, the user is + // shown information about the corresponding microtask. + + await tester.tap(find.text(testMicrotask!.id.toString())); + await tester.pumpAndSettle(); + + expect(find.byType(MicrotaskStackTraceView), findsOneWidget); + }); + }); +} diff --git a/packages/devtools_app/test/screens/vm_developer/vm_developer_test_utils.dart b/packages/devtools_app/test/screens/vm_developer/vm_developer_test_utils.dart index 1bd1b373c74..7506a057dda 100644 --- a/packages/devtools_app/test/screens/vm_developer/vm_developer_test_utils.dart +++ b/packages/devtools_app/test/screens/vm_developer/vm_developer_test_utils.dart @@ -240,3 +240,15 @@ void mockVmObject(VmObject object) { when(object.vmName).thenReturn(null); } } + +final testMicrotask = Microtask.parse({ + 'type': 'Microtask', + 'id': 123, + 'stackTrace': 'stack trace', +}); + +final testQueuedMicrotasks = QueuedMicrotasks.parse({ + 'type': 'QueuedMicrotasks', + 'timestamp': DateTime(2001).microsecondsSinceEpoch, + 'microtasks': [testMicrotask], +}); diff --git a/packages/devtools_app/test/test_infra/scenes/performance/default.dart b/packages/devtools_app/test/test_infra/scenes/performance/default.dart index 69634d8bf78..aec9c04b805 100644 --- a/packages/devtools_app/test/test_infra/scenes/performance/default.dart +++ b/packages/devtools_app/test/test_infra/scenes/performance/default.dart @@ -21,7 +21,7 @@ class PerformanceDefaultScene extends Scene { @override Widget build(BuildContext context) { return wrapWithControllers( - PerformanceScreenBody(showIsolateSelector: ValueNotifier(false)), + const PerformanceScreenBody(), performance: controller, ); } diff --git a/packages/devtools_app/test/test_infra/test_data/performance/sample_performance_data.dart b/packages/devtools_app/test/test_infra/test_data/performance/sample_performance_data.dart index b2ef0be0196..8a9362ca124 100644 --- a/packages/devtools_app/test/test_infra/test_data/performance/sample_performance_data.dart +++ b/packages/devtools_app/test/test_infra/test_data/performance/sample_performance_data.dart @@ -1359,15 +1359,3 @@ extension FlutterFrame6 on Never { endMicros: 713836331505, ); } - -final testMicrotask = Microtask.parse({ - 'type': 'Microtask', - 'id': 123, - 'stackTrace': 'stack trace', -}); - -final testQueuedMicrotasks = QueuedMicrotasks.parse({ - 'type': 'QueuedMicrotasks', - 'timestamp': DateTime(2001).microsecondsSinceEpoch, - 'microtasks': [testMicrotask], -}); diff --git a/packages/devtools_test/lib/src/mocks/generated.dart b/packages/devtools_test/lib/src/mocks/generated.dart index 5ab1c7ddc4c..a02acb34b43 100644 --- a/packages/devtools_test/lib/src/mocks/generated.dart +++ b/packages/devtools_test/lib/src/mocks/generated.dart @@ -21,7 +21,6 @@ import 'package:vm_service/vm_service.dart'; MockSpec(), MockSpec(), MockSpec(), - MockSpec(), MockSpec(), MockSpec(), MockSpec(), 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 843b15c89ee..f710b85b490 100644 --- a/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart +++ b/packages/devtools_test/lib/src/mocks/generated_mocks_factories.dart @@ -20,7 +20,6 @@ import 'generated.mocks.dart'; MockPerformanceController createMockPerformanceControllerWithDefaults() { final controller = MockPerformanceController(); final timelineEventsController = MockTimelineEventsController(); - final queuedMicrotasksController = MockQueuedMicrotasksController(); final flutterFramesController = MockFlutterFramesController(); when( controller.enhanceTracingController, @@ -49,11 +48,6 @@ MockPerformanceController createMockPerformanceControllerWithDefaults() { ValueNotifier(EventsControllerStatus.empty), ); - // Stubs for Queued Microtasks feature. - when( - controller.queuedMicrotasksController, - ).thenReturn(queuedMicrotasksController); - // Stubs for Rebuild Count feature when(controller.rebuildCountModel).thenReturn(RebuildCountModel()); when( From d59cca17eea455c93b682dbc4a1fe8c27750a3eb Mon Sep 17 00:00:00 2001 From: Derek Xu Date: Mon, 16 Jun 2025 12:51:41 -0400 Subject: [PATCH 4/4] Address comments --- .../queued_microtasks/queued_microtasks_view.dart | 4 ++++ .../release_notes/NEXT_RELEASE_NOTES.md | 14 +++++++++----- .../helpers/release_notes_template.md | 4 ++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/vm_developer/queued_microtasks/queued_microtasks_view.dart b/packages/devtools_app/lib/src/screens/vm_developer/queued_microtasks/queued_microtasks_view.dart index 1f177b8239e..f11e60d552b 100644 --- a/packages/devtools_app/lib/src/screens/vm_developer/queued_microtasks/queued_microtasks_view.dart +++ b/packages/devtools_app/lib/src/screens/vm_developer/queued_microtasks/queued_microtasks_view.dart @@ -64,6 +64,10 @@ class RefreshQueuedMicrotasksInstructions extends StatelessWidget { } } +/// Record containing details about a particular microtask that was in a +/// microtask queue snapshot, and an index representing how close to the front +/// of the queue that microtask was when the snapshot was taken. +/// /// In the response returned by the VM Service, microtasks are sorted in /// ascending order of when they will be dequeued, i.e. the microtask that will /// run earliest is at index 0 of the returned list. We use those indices of the diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index b4138e50df8..7122bb2d6e4 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -23,11 +23,7 @@ TODO: Remove this section if there are not any general updates. ## Performance updates -- Added a Queued Microtasks tab to the VM Tools screen, which allows a user to - see details about the microtasks scheduled in an isolate's microtask queue. - This tab currently only appears when DevTools is connected to a Flutter or - Dart app started with `--profile-microtasks`. - - [#9239](https://github.com/flutter/devtools/pull/9239). +TODO: Remove this section if there are not any general updates. ## CPU profiler updates @@ -65,6 +61,14 @@ TODO: Remove this section if there are not any general updates. TODO: Remove this section if there are not any general updates. +## Advanced developer mode updates + +- Added a Queued Microtasks tab to the VM Tools screen, which allows a user to + see details about the microtasks scheduled in an isolate's microtask queue. + This tab currently only appears when DevTools is connected to a Flutter or + Dart app started with `--profile-microtasks`. - + [#9239](https://github.com/flutter/devtools/pull/9239). + ## Full commit history To find a complete list of changes in this release, check out the diff --git a/packages/devtools_app/release_notes/helpers/release_notes_template.md b/packages/devtools_app/release_notes/helpers/release_notes_template.md index ac14a563bc4..afc1b1521ef 100644 --- a/packages/devtools_app/release_notes/helpers/release_notes_template.md +++ b/packages/devtools_app/release_notes/helpers/release_notes_template.md @@ -61,6 +61,10 @@ TODO: Remove this section if there are not any general updates. TODO: Remove this section if there are not any general updates. +## Advanced developer mode updates + +TODO: Remove this section if there are not any general updates. + ## Full commit history To find a complete list of changes in this release, check out the