Skip to content

Commit

Permalink
Add a setting to enable auto-refreshing the inspector tree (#8483)
Browse files Browse the repository at this point in the history
  • Loading branch information
elliette authored Oct 29, 2024
1 parent 0a752c8 commit 4630e23
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,17 @@ class FlutterInspectorSettingsDialog extends StatelessWidget {
'Hovering over any widget displays its properties and values.',
gaItem: gac.inspectorHoverEvalMode,
),
if (!inspectorV2Enabled) ...[
const SizedBox(height: largeSpacing),
const SizedBox(height: largeSpacing),
if (inspectorV2Enabled) ...[
CheckboxSetting(
notifier: preferences.inspector.autoRefreshEnabled
as ValueNotifier<bool?>,
title: 'Enable auto-refreshing of the widget tree',
description:
'The widget tree will automatically be refreshed after a hot-reload.',
gaItem: gac.inspectorAutoRefreshEnabled,
),
] else ...[
const InspectorDefaultDetailsViewOption(),
],
const SizedBox(height: largeSpacing),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import 'package:logging/logging.dart';
import 'package:vm_service/vm_service.dart';

import '../../service/service_extensions.dart' as extensions;
import '../../shared/analytics/analytics.dart' as ga;
import '../../shared/analytics/constants.dart' as gac;
import '../../shared/console/eval/inspector_tree_v2.dart';
import '../../shared/console/primitives/simple_items.dart';
import '../../shared/diagnostics/diagnostics_node.dart';
Expand Down Expand Up @@ -130,6 +132,17 @@ class InspectorController extends DisposableController
}

serviceConnection.consoleService.ensureServiceInitialized();

final vmService = serviceConnection.serviceManager.service;
if (vmService != null) {
autoDisposeStreamSubscription(
vmService.onIsolateEvent.listen(_maybeAutoRefreshInspector),
);

autoDisposeStreamSubscription(
vmService.onExtensionEvent.listen(_maybeAutoRefreshInspector),
);
}
}

void _handleConnectionStart() {
Expand Down Expand Up @@ -368,6 +381,26 @@ class InspectorController extends DisposableController
return _waitForPendingUpdateDone();
}

Future<void> refreshInspector() async {
// If the user is force refreshing the inspector before the first load has
// completed, this could indicate a slow load time or that the inspector
// failed to load the tree once available.
if (!firstInspectorTreeLoadCompleted) {
// We do not want to complete this timing operation because the force
// refresh will skew the results.
ga.cancelTimingOperation(
InspectorScreen.id,
gac.pageReady,
);
ga.select(
gac.inspector,
gac.refreshEmptyTree,
);
firstInspectorTreeLoadCompleted = true;
}
await onForceRefresh();
}

void filterErrors() {
serviceConnection.errorBadgeManager.filterErrors(
InspectorScreen.id,
Expand Down Expand Up @@ -417,6 +450,27 @@ class InspectorController extends DisposableController
}
}

bool _receivedIsolateReloadEvent = false;

Future<void> _maybeAutoRefreshInspector(Event event) async {
if (!preferences.inspector.autoRefreshEnabled.value) return;

// It is not sufficent to wait for the isolate reload event, because Flutter
// might not have re-painted the app. Instead, we need to wait for the first
// frame AFTER the isolate reload event in order to request the new tree.
if (event.kind == EventKind.kExtension) {
if (!_receivedIsolateReloadEvent) return;
if (event.extensionKind == 'Flutter.Frame') {
_receivedIsolateReloadEvent = false;
await refreshInspector();
}
}

if (event.kind == EventKind.kIsolateReload) {
_receivedIsolateReloadEvent = true;
}
}

Future<void> _recomputeTreeRoot(
RemoteDiagnosticsNode? newSelection, {
bool? hideImplementationWidgets,
Expand Down Expand Up @@ -451,8 +505,9 @@ class InspectorController extends DisposableController
expandChildren: true,
);
inspectorTree.root = rootNode;

refreshSelection(newSelection);
final selectedNode =
_determineNewSelection(newSelection ?? selectedDiagnostic);
refreshSelection(selectedNode);
_implementationWidgetsHidden.value = hideImplementationWidgets;
} catch (error, st) {
_log.shout(error, error, st);
Expand All @@ -461,6 +516,77 @@ class InspectorController extends DisposableController
}
}

RemoteDiagnosticsNode? _determineNewSelection(
RemoteDiagnosticsNode? previousSelection,
) {
if (previousSelection == null) return null;
if (valueToInspectorTreeNode.containsKey(previousSelection.valueRef)) {
return previousSelection;
}

// TODO(https://github.com/flutter/devtools/issues/8481): Consider using a
// variation of a path-finding algorithm to determine the new selection,
// instead of looking for the first matching descendant.
final (closestUnchangedAncestor, distanceToAncestor) =
_findClosestUnchangedAncestor(previousSelection);
if (closestUnchangedAncestor == null) return inspectorTree.root?.diagnostic;

const distanceOffset = 3;
final matchingDescendant = _findMatchingDescendant(
of: closestUnchangedAncestor,
matching: previousSelection,
inRange: Range(
distanceToAncestor - distanceOffset,
distanceToAncestor + distanceOffset,
),
);

return matchingDescendant ?? closestUnchangedAncestor;
}

(RemoteDiagnosticsNode?, int) _findClosestUnchangedAncestor(
RemoteDiagnosticsNode node, [
int distanceToAncestor = 1,
]) {
final inspectorTreeNode = valueToInspectorTreeNode[node.valueRef];
if (inspectorTreeNode != null) {
return (inspectorTreeNode.diagnostic, distanceToAncestor);
}

final ancestor = node.parent;
if (ancestor == null) return (null, distanceToAncestor);
return _findClosestUnchangedAncestor(ancestor, distanceToAncestor++);
}

RemoteDiagnosticsNode? _findMatchingDescendant({
required RemoteDiagnosticsNode of,
required RemoteDiagnosticsNode matching,
required Range inRange,
int currentDistance = 1,
}) {
if (currentDistance > inRange.end) return null;

if (inRange.contains(currentDistance)) {
if (of.description == matching.description) {
return of;
}
}

final children = of.childrenNow;
final distance = currentDistance++;
for (final child in children) {
final matchingDescendant = _findMatchingDescendant(
of: child,
matching: matching,
inRange: inRange,
currentDistance: distance,
);
if (matchingDescendant != null) return matchingDescendant;
}

return null;
}

Future<void> toggleImplementationWidgetsVisibility() async {
final root = inspectorTree.root?.diagnostic;
if (root != null) {
Expand Down Expand Up @@ -512,7 +638,7 @@ class InspectorController extends DisposableController
final matchingNode = findMatchingInspectorTreeNode(newSelection);
if (matchingNode != null) {
setSelectedNode(matchingNode);
syncSelectionHelper(selection: newSelection);
syncSelectionHelper(selection: matchingNode.diagnostic);

syncTreeSelection();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,23 +212,7 @@ class InspectorScreenBodyState extends State<InspectorScreenBody>
ga.select(gac.inspector, gac.refresh);
unawaited(
blockWhileInProgress(() async {
// If the user is force refreshing the inspector before the first load has
// completed, this could indicate a slow load time or that the inspector
// failed to load the tree once available.
if (!controller.firstInspectorTreeLoadCompleted) {
// We do not want to complete this timing operation because the force
// refresh will skew the results.
ga.cancelTimingOperation(
InspectorScreen.id,
gac.pageReady,
);
ga.select(
gac.inspector,
gac.refreshEmptyTree,
);
controller.firstInspectorTreeLoadCompleted = true;
}
await controller.onForceRefresh();
await controller.refreshInspector();
}),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ const wasm = 'wasm';
const verboseLogging = 'verboseLogging';
const inspectorHoverEvalMode = 'inspectorHoverEvalMode';
const inspectorV2Enabled = 'inspectorV2Enabled';
const inspectorAutoRefreshEnabled = 'inspectorAutoRefreshEnabled';
const clearLogs = 'clearLogs';
const copyLogs = 'copyLogs';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class InspectorPreferencesController extends DisposableController
with AutoDisposeControllerMixin {
ValueListenable<bool> get hoverEvalModeEnabled => _hoverEvalMode;
ValueListenable<bool> get inspectorV2Enabled => _inspectorV2Enabled;
ValueListenable<bool> get autoRefreshEnabled => _autoRefreshEnabled;
ValueListenable<InspectorDetailsViewType> get defaultDetailsView =>
_defaultDetailsView;
ListValueNotifier<String> get pubRootDirectories => _pubRootDirectories;
Expand All @@ -30,6 +31,9 @@ class InspectorPreferencesController extends DisposableController

final _hoverEvalMode = ValueNotifier<bool>(false);
final _inspectorV2Enabled = ValueNotifier<bool>(false);
// TODO(https://github.com/flutter/devtools/issues/1423): Default to true
// after verifying auto-refreshes are performant.
final _autoRefreshEnabled = ValueNotifier<bool>(false);
final _pubRootDirectories = ListValueNotifier<String>([]);
final _pubRootDirectoriesAreBusy = ValueNotifier<bool>(false);
final _busyCounter = ValueNotifier<int>(0);
Expand All @@ -39,6 +43,7 @@ class InspectorPreferencesController extends DisposableController

static const _hoverEvalModeStorageId = 'inspector.hoverEvalMode';
static const _inspectorV2EnabledStorageId = 'inspector.inspectorV2Enabled';
static const _autoRefreshEnabledStorageId = 'inspector.autoRefreshEnabled';
static const _defaultDetailsViewStorageId =
'inspector.defaultDetailsViewType';
static const _customPubRootDirectoriesStoragePrefix =
Expand All @@ -62,6 +67,7 @@ class InspectorPreferencesController extends DisposableController
Future<void> init() async {
await _initHoverEvalMode();
await _initInspectorV2Enabled();
await _initAutoRefreshEnabled();
// TODO(jacobr): consider initializing this first as it is not blocking.
_initPubRootDirectories();
await _initDefaultInspectorDetailsView();
Expand All @@ -83,6 +89,14 @@ class InspectorPreferencesController extends DisposableController
);
}

Future<void> _initAutoRefreshEnabled() async {
await _updateAutoRefreshEnabled();
_saveBooleanPreferenceChanges(
preferenceStorageId: _autoRefreshEnabledStorageId,
preferenceNotifier: _autoRefreshEnabled,
);
}

Future<void> _updateHoverEvalMode() async {
await _updateBooleanPreference(
preferenceStorageId: _hoverEvalModeStorageId,
Expand All @@ -99,6 +113,16 @@ class InspectorPreferencesController extends DisposableController
);
}

Future<void> _updateAutoRefreshEnabled() async {
await _updateBooleanPreference(
preferenceStorageId: _autoRefreshEnabledStorageId,
preferenceNotifier: _autoRefreshEnabled,
// TODO(https://github.com/flutter/devtools/issues/1423): Default to true
// after verifying auto-refreshes are performant.
defaultValue: false,
);
}

void _saveBooleanPreferenceChanges({
required String preferenceStorageId,
required ValueNotifier<bool> preferenceNotifier,
Expand Down Expand Up @@ -421,6 +445,11 @@ class InspectorPreferencesController extends DisposableController
_inspectorV2Enabled.value = inspectorV2Enabled;
}

@visibleForTesting
void setAutoRefreshEnabled(bool autoRefreshEnabled) {
_autoRefreshEnabled.value = autoRefreshEnabled;
}

void setDefaultInspectorDetailsView(InspectorDetailsViewType value) {
_defaultDetailsView.value = value;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ To learn more about DevTools, check out the

## Inspector updates

TODO: Remove this section if there are not any general updates.
- Added an option to the [new Inspector's](https://docs.flutter.dev/tools/devtools/release-notes/release-notes-2.40.1#inspector-updates)
settings to allow auto-refreshing the widget tree after a hot-reload. - [#8483](https://github.com/flutter/devtools/pull/8483)

## Performance updates

Expand Down

0 comments on commit 4630e23

Please sign in to comment.