Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/devtools_app/lib/devtools_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,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';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// 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';

enum QueuedMicrotasksControllerStatus { empty, refreshing, ready }

class QueuedMicrotasksController extends DisposableController
with AutoDisposeControllerMixin {
QueuedMicrotasksController() {
addAutoDisposeListener(_refreshWorkTracker.active, () {
final active = _refreshWorkTracker.active.value;
if (active) {
_status.value = QueuedMicrotasksControllerStatus.refreshing;
} else {
_status.value = QueuedMicrotasksControllerStatus.ready;
}
});
}

ValueListenable<QueuedMicrotasksControllerStatus> get status => _status;
final _status = ValueNotifier<QueuedMicrotasksControllerStatus>(
QueuedMicrotasksControllerStatus.empty,
);

ValueListenable<QueuedMicrotasks?> get queuedMicrotasks => _queuedMicrotasks;
final _queuedMicrotasks = ValueNotifier<QueuedMicrotasks?>(null);

ValueListenable<Microtask?> get selectedMicrotask => _selectedMicrotask;
final _selectedMicrotask = ValueNotifier<Microtask?>(null);

final _refreshWorkTracker = FutureWorkTracker();

Future<void> 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
void dispose() {
_status.dispose();
_queuedMicrotasks.dispose();
_selectedMicrotask.dispose();
_refreshWorkTracker
..clear()
..dispose();
super.dispose();
}
}
Original file line number Diff line number Diff line change
@@ -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<QueuedMicrotasksControllerStatus>(
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<IndexedMicrotask> {
_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<IndexedMicrotask> indexedMicrotasks,
required void Function(Microtask?) onMicrotaskSelected,
}) : _indexedMicrotasks = indexedMicrotasks,
_setSelectedMicrotask = onMicrotaskSelected;

static final _idColumn = _MicrotaskIdColumn();
final List<IndexedMicrotask> _indexedMicrotasks;
final void Function(Microtask?) _setSelectedMicrotask;

@override
Widget build(BuildContext context) => FlatTable<IndexedMicrotask>(
keyFactory: (IndexedMicrotask microtask) => ValueKey<int>(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,
),
),
),
],
),
),
],
);
},
);
}
},
),
),
),
],
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 = <VMDeveloperView>[
const VMStatisticsView(),
const IsolateStatisticsView(),
ObjectInspectorView(),
const VMProcessMemoryView(),
if (showQueuedMicrotasks) const QueuedMicrotasksView(),
];

@override
Expand Down
3 changes: 3 additions & 0 deletions packages/devtools_app/lib/src/service/vm_flags.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
const profileMicrotasks = 'profile_microtasks';

class VmFlagManager with DisposerMixin {
VmServiceWrapper get service => _service;
late VmServiceWrapper _service;
Expand Down
Loading
Loading