Skip to content

Commit b3c3d89

Browse files
committed
Add Queued Microtasks tab to Performance View
1 parent 0b5e328 commit b3c3d89

File tree

17 files changed

+651
-20
lines changed

17 files changed

+651
-20
lines changed

flutter-candidate.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
36ea2bdeab611e908967b6fa57659998f600a2cb
1+
ead0a01bb9707b60097679a43a6262a8979f4a89

packages/devtools_app/lib/devtools_app.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export 'src/screens/performance/panes/flutter_frames/flutter_frame_model.dart';
4141
export 'src/screens/performance/panes/flutter_frames/flutter_frames_chart.dart';
4242
export 'src/screens/performance/panes/flutter_frames/flutter_frames_controller.dart';
4343
export 'src/screens/performance/panes/frame_analysis/frame_analysis_model.dart';
44+
export 'src/screens/performance/panes/queued_microtasks/queued_microtasks_controller.dart';
4445
export 'src/screens/performance/panes/rebuild_stats/rebuild_stats_controller.dart';
4546
export 'src/screens/performance/panes/rebuild_stats/rebuild_stats_model.dart';
4647
export 'src/screens/performance/panes/timeline_events/perfetto/tracing/model.dart';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
import '../../performance_controller.dart';
14+
import '../../performance_model.dart';
15+
import '../flutter_frames/flutter_frame_model.dart';
16+
17+
enum QueuedMicrotasksControllerStatus { empty, refreshing, ready }
18+
19+
class QueuedMicrotasksController extends PerformanceFeatureController
20+
with AutoDisposeControllerMixin {
21+
QueuedMicrotasksController(super.controller) {
22+
addAutoDisposeListener(_refreshWorkTracker.active, () {
23+
final active = _refreshWorkTracker.active.value;
24+
if (active) {
25+
_status.value = QueuedMicrotasksControllerStatus.refreshing;
26+
} else {
27+
_status.value = QueuedMicrotasksControllerStatus.ready;
28+
}
29+
});
30+
}
31+
32+
final _status = ValueNotifier<QueuedMicrotasksControllerStatus>(
33+
QueuedMicrotasksControllerStatus.empty,
34+
);
35+
ValueListenable<QueuedMicrotasksControllerStatus> get status => _status;
36+
37+
final _queuedMicrotasks = ValueNotifier<QueuedMicrotasks?>(null);
38+
ValueListenable<QueuedMicrotasks?> get queuedMicrotasks => _queuedMicrotasks;
39+
40+
final _selectedMicrotask = ValueNotifier<Microtask?>(null);
41+
ValueListenable<Microtask?> get selectedMicrotask => _selectedMicrotask;
42+
43+
final _refreshWorkTracker = FutureWorkTracker();
44+
45+
@override
46+
void onBecomingActive() {}
47+
48+
Future<void> refresh() => _refreshWorkTracker.track(() async {
49+
_selectedMicrotask.value = null;
50+
51+
final isolateId =
52+
serviceConnection
53+
.serviceManager
54+
.isolateManager
55+
.selectedIsolate
56+
.value!
57+
.id!;
58+
final queuedMicrotasks = await serviceConnection.serviceManager.service!
59+
.getQueuedMicrotasks(isolateId);
60+
_queuedMicrotasks.value = queuedMicrotasks;
61+
62+
return;
63+
});
64+
65+
void setSelectedMicrotask(Microtask? microtask) {
66+
_selectedMicrotask.value = microtask;
67+
}
68+
69+
@override
70+
Future<void> handleSelectedFrame(FlutterFrame frame) async {}
71+
72+
@override
73+
Future<void> setOfflineData(OfflinePerformanceData offlineData) async {}
74+
75+
@override
76+
Future<void> clearData({bool partial = false}) async {
77+
_selectedMicrotask.value = null;
78+
_queuedMicrotasks.value = null;
79+
}
80+
81+
@override
82+
void dispose() {
83+
_status.dispose();
84+
_queuedMicrotasks.dispose();
85+
_selectedMicrotask.dispose();
86+
_refreshWorkTracker
87+
..clear()
88+
..dispose();
89+
super.dispose();
90+
}
91+
}
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
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:devtools_app_shared/ui.dart';
6+
import 'package:devtools_app_shared/utils.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' show RefreshButton;
16+
import 'queued_microtasks_controller.dart';
17+
18+
class RefreshQueuedMicrotasksButton extends StatelessWidget {
19+
const RefreshQueuedMicrotasksButton({
20+
super.key,
21+
required QueuedMicrotasksController controller,
22+
}) : _controller = controller;
23+
24+
final QueuedMicrotasksController _controller;
25+
26+
@override
27+
Widget build(BuildContext context) {
28+
return ValueListenableBuilder<QueuedMicrotasksControllerStatus>(
29+
valueListenable: _controller.status,
30+
builder: (_, status, _) {
31+
return RefreshButton(
32+
iconOnly: true,
33+
outlined: false,
34+
onPressed:
35+
status == QueuedMicrotasksControllerStatus.refreshing
36+
? null
37+
: _controller.refresh,
38+
tooltip:
39+
"Take a new snapshot of the selected isolate's microtask queue.",
40+
gaScreen: gac.performance,
41+
gaSelection: gac.PerformanceEvents.refreshQueuedMicrotasks.name,
42+
);
43+
},
44+
);
45+
}
46+
}
47+
48+
class QueuedMicrotasksTabControls extends StatelessWidget {
49+
const QueuedMicrotasksTabControls({
50+
super.key,
51+
required QueuedMicrotasksController controller,
52+
}) : _controller = controller;
53+
54+
final QueuedMicrotasksController _controller;
55+
56+
@override
57+
Widget build(BuildContext context) {
58+
return Row(
59+
mainAxisAlignment: MainAxisAlignment.end,
60+
children: [RefreshQueuedMicrotasksButton(controller: _controller)],
61+
);
62+
}
63+
}
64+
65+
class RefreshQueuedMicrotasksInstructions extends StatelessWidget {
66+
const RefreshQueuedMicrotasksInstructions({super.key});
67+
68+
@override
69+
Widget build(BuildContext context) {
70+
return Center(
71+
child: RichText(
72+
text: TextSpan(
73+
style: Theme.of(context).regularTextStyle,
74+
children: [
75+
const TextSpan(text: 'Click the refresh button '),
76+
WidgetSpan(child: Icon(Icons.refresh, size: defaultIconSize)),
77+
const TextSpan(
78+
text:
79+
" to take a new snapshot of the selected isolate's "
80+
'microtask queue.',
81+
),
82+
],
83+
),
84+
),
85+
);
86+
}
87+
}
88+
89+
// In the response returned by the VM Service, microtasks are sorted in
90+
// ascending order of when they will be dequeued, i.e. the microtask that will
91+
// run earliest is at index 0 of the returned list. We use those indices of the
92+
// returned list to sort the entries of the microtask selector, so that they
93+
// they also appear in ascending order of when they will be dequeued.
94+
typedef IndexedMicrotask = (int, Microtask);
95+
96+
class _MicrotaskIdColumn extends ColumnData<IndexedMicrotask> {
97+
_MicrotaskIdColumn()
98+
: super.wide('Microtask ID', alignment: ColumnAlignment.center);
99+
100+
@override
101+
int getValue(IndexedMicrotask indexedMicrotask) => indexedMicrotask.$1;
102+
103+
@override
104+
String getDisplayValue(IndexedMicrotask indexedMicrotask) =>
105+
indexedMicrotask.$2.id!.toString();
106+
}
107+
108+
class QueuedMicrotaskSelector extends StatelessWidget {
109+
const QueuedMicrotaskSelector({
110+
super.key,
111+
required List<IndexedMicrotask> indexedMicrotasks,
112+
required void Function(Microtask?) setSelectedMicrotask,
113+
}) : _indexedMicrotasks = indexedMicrotasks,
114+
_setSelectedMicrotask = setSelectedMicrotask;
115+
116+
static final _idColumn = _MicrotaskIdColumn();
117+
final List<IndexedMicrotask> _indexedMicrotasks;
118+
final void Function(Microtask?) _setSelectedMicrotask;
119+
120+
@override
121+
Widget build(BuildContext context) => Column(
122+
crossAxisAlignment: CrossAxisAlignment.start,
123+
children: [
124+
Expanded(
125+
child: FlatTable<IndexedMicrotask>(
126+
keyFactory:
127+
(IndexedMicrotask microtask) => ValueKey<int>(microtask.$1),
128+
data: _indexedMicrotasks,
129+
dataKey: 'queued-microtask-selector',
130+
columns: [_idColumn],
131+
defaultSortColumn: _idColumn,
132+
defaultSortDirection: SortDirection.ascending,
133+
onItemSelected:
134+
(indexedMicrotask) => _setSelectedMicrotask(indexedMicrotask?.$2),
135+
),
136+
),
137+
],
138+
);
139+
}
140+
141+
class StackTraceView extends StatelessWidget {
142+
const StackTraceView({super.key, required selectedMicrotask})
143+
: _selectedMicrotask = selectedMicrotask;
144+
145+
final Microtask? _selectedMicrotask;
146+
147+
@override
148+
Widget build(BuildContext context) {
149+
final theme = Theme.of(context);
150+
151+
return Column(
152+
children: [
153+
SizedBox.fromSize(
154+
size: Size.fromHeight(defaultHeaderHeight),
155+
child: Container(
156+
decoration: BoxDecoration(
157+
border: Border(bottom: defaultBorderSide(theme)),
158+
),
159+
padding: const EdgeInsets.only(left: defaultSpacing),
160+
alignment: Alignment.centerLeft,
161+
child: const Row(
162+
children: [
163+
Text('Stack trace captured when microtask was enqueued'),
164+
],
165+
),
166+
),
167+
),
168+
Row(
169+
children: [
170+
Expanded(
171+
child: Padding(
172+
padding: const EdgeInsets.symmetric(
173+
vertical: denseRowSpacing,
174+
horizontal: defaultSpacing,
175+
),
176+
child: SelectableText(
177+
style: theme.fixedFontStyle,
178+
_selectedMicrotask!.stackTrace.toString(),
179+
),
180+
),
181+
),
182+
],
183+
),
184+
],
185+
);
186+
}
187+
}
188+
189+
class QueuedMicrotasksTabView extends StatefulWidget {
190+
const QueuedMicrotasksTabView({super.key, required this.controller});
191+
192+
final QueuedMicrotasksController controller;
193+
194+
@override
195+
State<QueuedMicrotasksTabView> createState() =>
196+
_QueuedMicrotasksTabViewState();
197+
}
198+
199+
class _QueuedMicrotasksTabViewState extends State<QueuedMicrotasksTabView>
200+
with AutoDisposeMixin {
201+
static final _dateTimeFormat = DateFormat('HH:mm:ss.SSS (MM/dd/yy)');
202+
203+
@override
204+
Widget build(BuildContext context) {
205+
return ValueListenableBuilder(
206+
valueListenable: widget.controller.status,
207+
builder: (context, status, _) {
208+
if (status == QueuedMicrotasksControllerStatus.empty) {
209+
return const RefreshQueuedMicrotasksInstructions();
210+
} else if (status == QueuedMicrotasksControllerStatus.refreshing) {
211+
return Center(
212+
child: Text(
213+
style: Theme.of(context).regularTextStyle,
214+
'Refreshing...',
215+
),
216+
);
217+
} else {
218+
return ValueListenableBuilder(
219+
valueListenable: widget.controller.queuedMicrotasks,
220+
builder: (_, queuedMicrotasks, _) {
221+
assert(queuedMicrotasks != null);
222+
223+
final List<IndexedMicrotask> indexedMicrotasks =
224+
queuedMicrotasks!.microtasks!.indexed
225+
.cast<IndexedMicrotask>()
226+
.toList();
227+
final formattedTimestamp = _dateTimeFormat.format(
228+
DateTime.fromMicrosecondsSinceEpoch(
229+
queuedMicrotasks.timestamp!,
230+
),
231+
);
232+
return Column(
233+
crossAxisAlignment: CrossAxisAlignment.start,
234+
children: [
235+
Padding(
236+
padding: const EdgeInsets.symmetric(
237+
vertical: denseRowSpacing,
238+
horizontal: defaultSpacing,
239+
),
240+
child: Text(
241+
'Viewing snapshot that was taken at $formattedTimestamp.',
242+
),
243+
),
244+
Expanded(
245+
child: SplitPane(
246+
axis: Axis.horizontal,
247+
initialFractions: const [0.15, 0.85],
248+
children: [
249+
QueuedMicrotaskSelector(
250+
indexedMicrotasks: indexedMicrotasks,
251+
setSelectedMicrotask:
252+
widget.controller.setSelectedMicrotask,
253+
),
254+
ValueListenableBuilder(
255+
valueListenable: widget.controller.selectedMicrotask,
256+
builder:
257+
(_, selectedMicrotask, _) =>
258+
selectedMicrotask == null
259+
? const Center(
260+
child: Text(
261+
'Select a microtask ID on the left '
262+
'to see information about the '
263+
'corresponding microtask.',
264+
),
265+
)
266+
: StackTraceView(
267+
selectedMicrotask: selectedMicrotask,
268+
),
269+
),
270+
],
271+
),
272+
),
273+
],
274+
);
275+
},
276+
);
277+
}
278+
},
279+
);
280+
}
281+
}

0 commit comments

Comments
 (0)