Skip to content

Commit d85106e

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

File tree

17 files changed

+648
-21
lines changed

17 files changed

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

0 commit comments

Comments
 (0)