Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,275 @@
// 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.',
),
],
),
),
);
}
}

/// 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
/// 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
Loading