Skip to content

Commit 852c2e4

Browse files
authored
Add Queued Microtasks tab to VM Tools screen (#9239)
1 parent 5436ea4 commit 852c2e4

13 files changed

Lines changed: 515 additions & 0 deletions

File tree

packages/devtools_app/lib/devtools_app.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export 'src/screens/vm_developer/object_inspector/class_hierarchy_explorer.dart'
5757
export 'src/screens/vm_developer/object_inspector/class_hierarchy_explorer_controller.dart';
5858
export 'src/screens/vm_developer/object_inspector/object_inspector_view_controller.dart';
5959
export 'src/screens/vm_developer/object_inspector/vm_object_model.dart';
60+
export 'src/screens/vm_developer/queued_microtasks/queued_microtasks_controller.dart';
6061
export 'src/screens/vm_developer/vm_developer_tools_controller.dart';
6162
export 'src/screens/vm_developer/vm_developer_tools_screen.dart';
6263
export 'src/screens/vm_developer/vm_service_private_extensions.dart';
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright 2025 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
4+
5+
import 'dart:async';
6+
7+
import 'package:devtools_app_shared/utils.dart';
8+
import 'package:flutter/foundation.dart';
9+
import 'package:vm_service/vm_service.dart';
10+
11+
import '../../../shared/globals.dart';
12+
import '../../../shared/utils/future_work_tracker.dart';
13+
14+
enum QueuedMicrotasksControllerStatus { empty, refreshing, ready }
15+
16+
class QueuedMicrotasksController extends DisposableController
17+
with AutoDisposeControllerMixin {
18+
QueuedMicrotasksController() {
19+
addAutoDisposeListener(_refreshWorkTracker.active, () {
20+
final active = _refreshWorkTracker.active.value;
21+
if (active) {
22+
_status.value = QueuedMicrotasksControllerStatus.refreshing;
23+
} else {
24+
_status.value = QueuedMicrotasksControllerStatus.ready;
25+
}
26+
});
27+
}
28+
29+
ValueListenable<QueuedMicrotasksControllerStatus> get status => _status;
30+
final _status = ValueNotifier<QueuedMicrotasksControllerStatus>(
31+
QueuedMicrotasksControllerStatus.empty,
32+
);
33+
34+
ValueListenable<QueuedMicrotasks?> get queuedMicrotasks => _queuedMicrotasks;
35+
final _queuedMicrotasks = ValueNotifier<QueuedMicrotasks?>(null);
36+
37+
ValueListenable<Microtask?> get selectedMicrotask => _selectedMicrotask;
38+
final _selectedMicrotask = ValueNotifier<Microtask?>(null);
39+
40+
final _refreshWorkTracker = FutureWorkTracker();
41+
42+
Future<void> refresh() => _refreshWorkTracker.track(() async {
43+
_selectedMicrotask.value = null;
44+
45+
final isolateId = serviceConnection
46+
.serviceManager
47+
.isolateManager
48+
.selectedIsolate
49+
.value!
50+
.id!;
51+
final queuedMicrotasks = await serviceConnection.serviceManager.service!
52+
.getQueuedMicrotasks(isolateId);
53+
_queuedMicrotasks.value = queuedMicrotasks;
54+
55+
return;
56+
});
57+
58+
void setSelectedMicrotask(Microtask? microtask) {
59+
_selectedMicrotask.value = microtask;
60+
}
61+
62+
@override
63+
void dispose() {
64+
_status.dispose();
65+
_queuedMicrotasks.dispose();
66+
_selectedMicrotask.dispose();
67+
_refreshWorkTracker
68+
..clear()
69+
..dispose();
70+
super.dispose();
71+
}
72+
}
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
// Copyright 2025 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
4+
5+
import 'package:collection/collection.dart' show ListExtensions;
6+
import 'package:devtools_app_shared/ui.dart';
7+
import 'package:flutter/material.dart';
8+
import 'package:intl/intl.dart' show DateFormat;
9+
import 'package:vm_service/vm_service.dart';
10+
11+
import '../../../shared/analytics/constants.dart' as gac;
12+
import '../../../shared/primitives/utils.dart' show SortDirection;
13+
import '../../../shared/table/table.dart' show FlatTable;
14+
import '../../../shared/table/table_data.dart';
15+
import '../../../shared/ui/common_widgets.dart';
16+
import '../vm_developer_tools_screen.dart';
17+
import 'queued_microtasks_controller.dart';
18+
19+
class RefreshQueuedMicrotasksButton extends StatelessWidget {
20+
const RefreshQueuedMicrotasksButton({super.key, required this.controller});
21+
22+
final QueuedMicrotasksController controller;
23+
24+
@override
25+
Widget build(BuildContext context) {
26+
return ValueListenableBuilder<QueuedMicrotasksControllerStatus>(
27+
valueListenable: controller.status,
28+
builder: (_, status, _) {
29+
return RefreshButton(
30+
onPressed: status == QueuedMicrotasksControllerStatus.refreshing
31+
? null
32+
: controller.refresh,
33+
tooltip:
34+
"Take a new snapshot of the selected isolate's microtask queue.",
35+
gaScreen: gac.vmTools,
36+
gaSelection: gac.refreshQueuedMicrotasks,
37+
);
38+
},
39+
);
40+
}
41+
}
42+
43+
class RefreshQueuedMicrotasksInstructions extends StatelessWidget {
44+
const RefreshQueuedMicrotasksInstructions({super.key});
45+
46+
@override
47+
Widget build(BuildContext context) {
48+
return Center(
49+
child: RichText(
50+
text: TextSpan(
51+
style: Theme.of(context).regularTextStyle,
52+
children: [
53+
const TextSpan(text: 'Click the refresh button '),
54+
WidgetSpan(child: Icon(Icons.refresh, size: defaultIconSize)),
55+
const TextSpan(
56+
text:
57+
" to take a new snapshot of the selected isolate's "
58+
'microtask queue.',
59+
),
60+
],
61+
),
62+
),
63+
);
64+
}
65+
}
66+
67+
/// Record containing details about a particular microtask that was in a
68+
/// microtask queue snapshot, and an index representing how close to the front
69+
/// of the queue that microtask was when the snapshot was taken.
70+
///
71+
/// In the response returned by the VM Service, microtasks are sorted in
72+
/// ascending order of when they will be dequeued, i.e. the microtask that will
73+
/// run earliest is at index 0 of the returned list. We use those indices of the
74+
/// returned list to sort the entries of the microtask selector, so that they
75+
/// they also appear in ascending order of when they will be dequeued.
76+
typedef IndexedMicrotask = ({int index, Microtask microtask});
77+
78+
class _MicrotaskIdColumn extends ColumnData<IndexedMicrotask> {
79+
_MicrotaskIdColumn()
80+
: super.wide('Microtask ID', alignment: ColumnAlignment.center);
81+
82+
@override
83+
int getValue(IndexedMicrotask indexedMicrotask) => indexedMicrotask.index;
84+
85+
@override
86+
String getDisplayValue(IndexedMicrotask indexedMicrotask) =>
87+
indexedMicrotask.microtask.id!.toString();
88+
}
89+
90+
class QueuedMicrotaskSelector extends StatelessWidget {
91+
const QueuedMicrotaskSelector({
92+
super.key,
93+
required List<IndexedMicrotask> indexedMicrotasks,
94+
required void Function(Microtask?) onMicrotaskSelected,
95+
}) : _indexedMicrotasks = indexedMicrotasks,
96+
_setSelectedMicrotask = onMicrotaskSelected;
97+
98+
static final _idColumn = _MicrotaskIdColumn();
99+
final List<IndexedMicrotask> _indexedMicrotasks;
100+
final void Function(Microtask?) _setSelectedMicrotask;
101+
102+
@override
103+
Widget build(BuildContext context) => FlatTable<IndexedMicrotask>(
104+
keyFactory: (IndexedMicrotask microtask) => ValueKey<int>(microtask.index),
105+
data: _indexedMicrotasks,
106+
dataKey: 'queued-microtask-selector',
107+
columns: [_idColumn],
108+
defaultSortColumn: _idColumn,
109+
defaultSortDirection: SortDirection.ascending,
110+
onItemSelected: (indexedMicrotask) =>
111+
_setSelectedMicrotask(indexedMicrotask?.microtask),
112+
);
113+
}
114+
115+
class MicrotaskStackTraceView extends StatelessWidget {
116+
const MicrotaskStackTraceView({super.key, required selectedMicrotask})
117+
: _selectedMicrotask = selectedMicrotask;
118+
119+
final Microtask? _selectedMicrotask;
120+
121+
@override
122+
Widget build(BuildContext context) {
123+
final theme = Theme.of(context);
124+
125+
return Column(
126+
children: [
127+
Container(
128+
height: defaultHeaderHeight,
129+
padding: const EdgeInsets.only(left: defaultSpacing),
130+
alignment: Alignment.centerLeft,
131+
child: OutlineDecoration.onlyBottom(
132+
child: const Row(
133+
children: [
134+
Text('Stack trace captured when microtask was enqueued'),
135+
],
136+
),
137+
),
138+
),
139+
Row(
140+
children: [
141+
Expanded(
142+
child: Padding(
143+
padding: const EdgeInsets.symmetric(
144+
vertical: denseRowSpacing,
145+
horizontal: defaultSpacing,
146+
),
147+
child: SelectableText(
148+
style: theme.fixedFontStyle,
149+
_selectedMicrotask!.stackTrace.toString(),
150+
),
151+
),
152+
),
153+
],
154+
),
155+
],
156+
);
157+
}
158+
}
159+
160+
class QueuedMicrotasksView extends VMDeveloperView {
161+
const QueuedMicrotasksView()
162+
: super(title: 'Queued Microtasks', icon: Icons.pending_actions);
163+
164+
@override
165+
bool get showIsolateSelector => true;
166+
167+
@override
168+
Widget build(BuildContext context) => QueuedMicrotasksViewBody();
169+
}
170+
171+
class QueuedMicrotasksViewBody extends StatelessWidget {
172+
QueuedMicrotasksViewBody({super.key});
173+
174+
@visibleForTesting
175+
static final dateTimeFormat = DateFormat('HH:mm:ss.SSS (MM/dd/yy)');
176+
final controller = QueuedMicrotasksController();
177+
178+
@override
179+
Widget build(BuildContext context) {
180+
return Column(
181+
crossAxisAlignment: CrossAxisAlignment.start,
182+
children: [
183+
RefreshQueuedMicrotasksButton(controller: controller),
184+
const SizedBox(height: denseRowSpacing),
185+
Expanded(
186+
child: OutlineDecoration(
187+
child: ValueListenableBuilder(
188+
valueListenable: controller.status,
189+
builder: (context, status, _) {
190+
if (status == QueuedMicrotasksControllerStatus.empty) {
191+
return const RefreshQueuedMicrotasksInstructions();
192+
} else if (status ==
193+
QueuedMicrotasksControllerStatus.refreshing) {
194+
return const CenteredMessage(message: 'Refreshing...');
195+
} else {
196+
return ValueListenableBuilder(
197+
valueListenable: controller.queuedMicrotasks,
198+
builder: (_, queuedMicrotasks, _) {
199+
assert(queuedMicrotasks != null);
200+
if (queuedMicrotasks == null) {
201+
return const CenteredMessage(
202+
message: 'Unexpected null value',
203+
);
204+
}
205+
206+
final indexedMicrotasks = queuedMicrotasks.microtasks!
207+
.mapIndexed(
208+
(index, microtask) =>
209+
(index: index, microtask: microtask),
210+
)
211+
.toList();
212+
final formattedTimestamp = dateTimeFormat.format(
213+
DateTime.fromMicrosecondsSinceEpoch(
214+
queuedMicrotasks.timestamp!,
215+
),
216+
);
217+
return Column(
218+
crossAxisAlignment: CrossAxisAlignment.start,
219+
children: [
220+
Padding(
221+
padding: const EdgeInsets.symmetric(
222+
vertical: denseRowSpacing,
223+
horizontal: defaultSpacing,
224+
),
225+
child: Text(
226+
'Viewing snapshot that was taken at '
227+
'$formattedTimestamp.',
228+
),
229+
),
230+
Expanded(
231+
child: SplitPane(
232+
axis: Axis.horizontal,
233+
initialFractions: const [0.15, 0.85],
234+
children: [
235+
OutlineDecoration(
236+
child: QueuedMicrotaskSelector(
237+
indexedMicrotasks: indexedMicrotasks,
238+
onMicrotaskSelected:
239+
controller.setSelectedMicrotask,
240+
),
241+
),
242+
ValueListenableBuilder(
243+
valueListenable: controller.selectedMicrotask,
244+
builder: (_, selectedMicrotask, _) =>
245+
OutlineDecoration(
246+
child: selectedMicrotask == null
247+
? const CenteredMessage(
248+
message:
249+
'Select a microtask ID on '
250+
'the left to see '
251+
'information about the '
252+
'corresponding microtask.',
253+
)
254+
: MicrotaskStackTraceView(
255+
selectedMicrotask:
256+
selectedMicrotask,
257+
),
258+
),
259+
),
260+
],
261+
),
262+
),
263+
],
264+
);
265+
},
266+
);
267+
}
268+
},
269+
),
270+
),
271+
),
272+
],
273+
);
274+
}
275+
}

packages/devtools_app/lib/src/screens/vm_developer/vm_developer_tools_screen.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import 'package:devtools_app_shared/ui.dart';
66
import 'package:flutter/foundation.dart';
77
import 'package:flutter/material.dart';
88

9+
import '../../service/vm_flags.dart' as vm_flags;
910
import '../../shared/framework/screen.dart';
1011
import '../../shared/globals.dart';
1112
import 'isolate_statistics/isolate_statistics_view.dart';
1213
import 'object_inspector/object_inspector_view.dart';
1314
import 'process_memory/process_memory_view.dart';
15+
import 'queued_microtasks/queued_microtasks_view.dart';
1416
import 'vm_developer_tools_controller.dart';
1517
import 'vm_statistics/vm_statistics_view.dart';
1618

@@ -49,11 +51,21 @@ class VMDeveloperToolsScreen extends Screen {
4951
class VMDeveloperToolsScreenBody extends StatelessWidget {
5052
const VMDeveloperToolsScreenBody({super.key});
5153

54+
// The value of the `--profile-microtasks` VM flag cannot be modified once
55+
// the VM has started running.
56+
static final showQueuedMicrotasks =
57+
serviceConnection.vmFlagManager
58+
.flag(vm_flags.profileMicrotasks)
59+
?.value
60+
.valueAsString ==
61+
'true';
62+
5263
static final views = <VMDeveloperView>[
5364
const VMStatisticsView(),
5465
const IsolateStatisticsView(),
5566
ObjectInspectorView(),
5667
const VMProcessMemoryView(),
68+
if (showQueuedMicrotasks) const QueuedMicrotasksView(),
5769
];
5870

5971
@override

0 commit comments

Comments
 (0)