Skip to content

Commit 21bea32

Browse files
authored
[Widget Inspector] Fix stack overflow error for Flutter web when requesting a large widget tree (#159454)
Fixes flutter/devtools#8553 Context: A Flutter web customer with a large widget tree was getting a stack overflow error when they toggled on "show implementation widgets" in the Flutter DevTools Inspector. This is because building the JSON tree recursively was hitting Chrome's stack limit. This PR creates the JSON tree **iteratively** if the `getRootWidgetTree` service extension is called with `fullDetails = false` (which is what DevTools uses to fetch the widget tree). For all other instances of creating a widget JSON map (for example, when fetching widget properties) the recursive implementation is used. This allows properties provided by subclasses implementing `toJsonMap` to be included in the response. Note: Because with this change `toJsonMap` is only called when `fullDetails = true` and `toJsonMapIterative` is only called when `fullDetails = false`, this PR partially reverts the changes in flutter/flutter#157309.
1 parent 4ff6698 commit 21bea32

File tree

7 files changed

+170
-154
lines changed

7 files changed

+170
-154
lines changed

packages/flutter/lib/src/foundation/diagnostics.dart

Lines changed: 136 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
/// @docImport 'package:flutter/widgets.dart';
99
library;
1010

11+
import 'dart:collection';
1112
import 'dart:math' as math;
1213
import 'dart:ui' show clampDouble;
1314

@@ -1421,6 +1422,17 @@ class TextTreeRenderer {
14211422
}
14221423
}
14231424

1425+
/// The JSON representation of a [DiagnosticsNode].
1426+
typedef _JsonDiagnosticsNode = Map<String, Object?>;
1427+
1428+
/// Stack containing [DiagnosticNode]s to convert to JSON and the callback to
1429+
/// call with the JSON.
1430+
///
1431+
/// Using a stack is required to process the widget tree iteratively instead of
1432+
/// recursively.
1433+
typedef _NodesToJsonifyStack
1434+
= ListQueue<(DiagnosticsNode, void Function(_JsonDiagnosticsNode))>;
1435+
14241436
/// Defines diagnostics data for a [value].
14251437
///
14261438
/// For debug and profile modes, [DiagnosticsNode] provides a high quality
@@ -1605,29 +1617,12 @@ abstract class DiagnosticsNode {
16051617
/// by this method and interactive tree views in the Flutter IntelliJ
16061618
/// plugin.
16071619
@mustCallSuper
1608-
Map<String, Object?> toJsonMap(
1609-
DiagnosticsSerializationDelegate delegate, {
1610-
bool fullDetails = true,
1611-
}) {
1620+
Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
16121621
Map<String, Object?> result = <String, Object?>{};
16131622
assert(() {
16141623
final bool hasChildren = getChildren().isNotEmpty;
1615-
final Map<String, Object?> essentialDetails = <String, Object?>{
1624+
result = <String, Object?>{
16161625
'description': toDescription(),
1617-
'shouldIndent': style != DiagnosticsTreeStyle.flat &&
1618-
style != DiagnosticsTreeStyle.error,
1619-
...delegate.additionalNodeProperties(this, fullDetails: fullDetails),
1620-
if (delegate.subtreeDepth > 0)
1621-
'children': toJsonList(
1622-
delegate.filterChildren(getChildren(), this),
1623-
this,
1624-
delegate,
1625-
fullDetails: fullDetails,
1626-
),
1627-
};
1628-
1629-
result = !fullDetails ? essentialDetails : <String, Object?>{
1630-
...essentialDetails,
16311626
'type': runtimeType.toString(),
16321627
if (name != null)
16331628
'name': name,
@@ -1651,19 +1646,54 @@ abstract class DiagnosticsNode {
16511646
'allowWrap': allowWrap,
16521647
if (allowNameWrap)
16531648
'allowNameWrap': allowNameWrap,
1649+
...delegate.additionalNodeProperties(this),
16541650
if (delegate.includeProperties)
16551651
'properties': toJsonList(
16561652
delegate.filterProperties(getProperties(), this),
16571653
this,
16581654
delegate,
1659-
fullDetails: fullDetails,
1655+
),
1656+
if (delegate.subtreeDepth > 0)
1657+
'children': toJsonList(
1658+
delegate.filterChildren(getChildren(), this),
1659+
this,
1660+
delegate,
16601661
),
16611662
};
16621663
return true;
16631664
}());
16641665
return result;
16651666
}
16661667

1668+
/// Iteratively serialize the node to a JSON map according to the
1669+
/// configuration provided in the [DiagnosticsSerializationDelegate].
1670+
///
1671+
/// This is only used when [WidgetInspectorServiceExtensions.getRootWidgetTree]
1672+
/// is called with fullDetails=false. To get the full widget details, including
1673+
/// any details provided by subclasses, [toJsonMap] should be used instead.
1674+
///
1675+
/// See https://github.com/flutter/devtools/issues/8553 for details about this
1676+
/// iterative approach.
1677+
Map<String, Object?> toJsonMapIterative(
1678+
DiagnosticsSerializationDelegate delegate,
1679+
) {
1680+
final _NodesToJsonifyStack childrenToJsonify =
1681+
ListQueue<(DiagnosticsNode, void Function(_JsonDiagnosticsNode))>();
1682+
_JsonDiagnosticsNode result = <String, Object?>{};
1683+
assert(() {
1684+
result = _toJson(
1685+
delegate,
1686+
childrenToJsonify: childrenToJsonify,
1687+
);
1688+
_jsonifyNextNodesInStack(
1689+
childrenToJsonify,
1690+
delegate: delegate,
1691+
);
1692+
return true;
1693+
}());
1694+
return result;
1695+
}
1696+
16671697
/// Serializes a [List] of [DiagnosticsNode]s to a JSON list according to
16681698
/// the configuration provided by the [DiagnosticsSerializationDelegate].
16691699
///
@@ -1672,9 +1702,8 @@ abstract class DiagnosticsNode {
16721702
static List<Map<String, Object?>> toJsonList(
16731703
List<DiagnosticsNode>? nodes,
16741704
DiagnosticsNode? parent,
1675-
DiagnosticsSerializationDelegate delegate, {
1676-
bool fullDetails = true,
1677-
}) {
1705+
DiagnosticsSerializationDelegate delegate,
1706+
) {
16781707
bool truncated = false;
16791708
if (nodes == null) {
16801709
return const <Map<String, Object?>>[];
@@ -1685,11 +1714,9 @@ abstract class DiagnosticsNode {
16851714
nodes.add(DiagnosticsNode.message('...'));
16861715
truncated = true;
16871716
}
1688-
final List<Map<String, Object?>> json = nodes.map<Map<String, Object?>>((DiagnosticsNode node) {
1689-
return node.toJsonMap(
1690-
delegate.delegateForNode(node),
1691-
fullDetails: fullDetails,
1692-
);
1717+
final List<_JsonDiagnosticsNode> json =
1718+
nodes.map<_JsonDiagnosticsNode>((DiagnosticsNode node) {
1719+
return node.toJsonMap(delegate.delegateForNode(node));
16931720
}).toList();
16941721
if (truncated) {
16951722
json.last['truncated'] = true;
@@ -1803,6 +1830,73 @@ abstract class DiagnosticsNode {
18031830
}());
18041831
return result;
18051832
}
1833+
1834+
void _jsonifyNextNodesInStack(
1835+
_NodesToJsonifyStack toJsonify, {
1836+
required DiagnosticsSerializationDelegate delegate,
1837+
}) {
1838+
while (toJsonify.isNotEmpty) {
1839+
final (
1840+
DiagnosticsNode nextNode,
1841+
void Function(_JsonDiagnosticsNode) callback
1842+
) = toJsonify.removeFirst();
1843+
final _JsonDiagnosticsNode nodeAsJson = nextNode._toJson(
1844+
delegate,
1845+
childrenToJsonify: toJsonify,
1846+
);
1847+
callback(nodeAsJson);
1848+
}
1849+
}
1850+
1851+
Map<String, Object?> _toJson(
1852+
DiagnosticsSerializationDelegate delegate, {
1853+
required _NodesToJsonifyStack childrenToJsonify,
1854+
}) {
1855+
final List<_JsonDiagnosticsNode> childrenJsonList =
1856+
<_JsonDiagnosticsNode>[];
1857+
final bool includeChildren =
1858+
getChildren().isNotEmpty && delegate.subtreeDepth > 0;
1859+
1860+
// Collect the children nodes to convert to JSON later.
1861+
bool truncated = false;
1862+
if (includeChildren) {
1863+
List<DiagnosticsNode> childrenNodes =
1864+
delegate.filterChildren(getChildren(), this);
1865+
final int originalNodeCount = childrenNodes.length;
1866+
childrenNodes = delegate.truncateNodesList(childrenNodes, this);
1867+
if (childrenNodes.length != originalNodeCount) {
1868+
childrenNodes.add(DiagnosticsNode.message('...'));
1869+
truncated = true;
1870+
}
1871+
for (final DiagnosticsNode child in childrenNodes) {
1872+
childrenToJsonify.add((
1873+
child,
1874+
(_JsonDiagnosticsNode jsonChild) {
1875+
childrenJsonList.add(jsonChild);
1876+
}
1877+
));
1878+
}
1879+
}
1880+
1881+
final String description = toDescription();
1882+
final String widgetRuntimeType =
1883+
description == '[root]' ? 'RootWidget' : description.split('-').first;
1884+
final bool shouldIndent = style != DiagnosticsTreeStyle.flat &&
1885+
style != DiagnosticsTreeStyle.error;
1886+
1887+
return <String, Object?>{
1888+
'description': description,
1889+
'shouldIndent': shouldIndent,
1890+
// TODO(elliette): This can be removed to reduce the JSON response even
1891+
// further once DevTools computes the widget runtime type from the
1892+
// description instead, see:
1893+
// https://github.com/flutter/devtools/issues/8556
1894+
'widgetRuntimeType': widgetRuntimeType,
1895+
'truncated': truncated,
1896+
...delegate.additionalNodeProperties(this, fullDetails: false),
1897+
if (includeChildren) 'children': childrenJsonList,
1898+
};
1899+
}
18061900
}
18071901

18081902
/// Debugging message displayed like a property.
@@ -1872,17 +1966,8 @@ class StringProperty extends DiagnosticsProperty<String> {
18721966
final bool quoted;
18731967

18741968
@override
1875-
Map<String, Object?> toJsonMap(
1876-
DiagnosticsSerializationDelegate delegate, {
1877-
bool fullDetails = true,
1878-
}) {
1879-
final Map<String, Object?> json = super.toJsonMap(
1880-
delegate,
1881-
fullDetails: fullDetails,
1882-
);
1883-
if (!fullDetails) {
1884-
return json;
1885-
}
1969+
Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
1970+
final Map<String, Object?> json = super.toJsonMap(delegate);
18861971
json['quoted'] = quoted;
18871972
return json;
18881973
}
@@ -1937,18 +2022,8 @@ abstract class _NumProperty<T extends num> extends DiagnosticsProperty<T> {
19372022
}) : super.lazy();
19382023

19392024
@override
1940-
Map<String, Object?> toJsonMap(
1941-
DiagnosticsSerializationDelegate delegate, {
1942-
bool fullDetails = true,
1943-
}) {
1944-
final Map<String, Object?> json = super.toJsonMap(
1945-
delegate,
1946-
fullDetails: fullDetails,
1947-
);
1948-
if (!fullDetails) {
1949-
return json;
1950-
}
1951-
2025+
Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
2026+
final Map<String, Object?> json = super.toJsonMap(delegate);
19522027
if (unit != null) {
19532028
json['unit'] = unit;
19542029
}
@@ -2131,17 +2206,8 @@ class FlagProperty extends DiagnosticsProperty<bool> {
21312206
);
21322207

21332208
@override
2134-
Map<String, Object?> toJsonMap(
2135-
DiagnosticsSerializationDelegate delegate, {
2136-
bool fullDetails = true,
2137-
}) {
2138-
final Map<String, Object?> json = super.toJsonMap(
2139-
delegate,
2140-
fullDetails: fullDetails,
2141-
);
2142-
if (!fullDetails) {
2143-
return json;
2144-
}
2209+
Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
2210+
final Map<String, Object?> json = super.toJsonMap(delegate);
21452211
if (ifTrue != null) {
21462212
json['ifTrue'] = ifTrue;
21472213
}
@@ -2262,17 +2328,8 @@ class IterableProperty<T> extends DiagnosticsProperty<Iterable<T>> {
22622328
}
22632329

22642330
@override
2265-
Map<String, Object?> toJsonMap(
2266-
DiagnosticsSerializationDelegate delegate, {
2267-
bool fullDetails = true,
2268-
}) {
2269-
final Map<String, Object?> json = super.toJsonMap(
2270-
delegate,
2271-
fullDetails: fullDetails,
2272-
);
2273-
if (!fullDetails) {
2274-
return json;
2275-
}
2331+
Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
2332+
final Map<String, Object?> json = super.toJsonMap(delegate);
22762333
if (value != null) {
22772334
json['values'] = value!.map<String>((T value) => value.toString()).toList();
22782335
}
@@ -2409,17 +2466,8 @@ class ObjectFlagProperty<T> extends DiagnosticsProperty<T> {
24092466
}
24102467

24112468
@override
2412-
Map<String, Object?> toJsonMap(
2413-
DiagnosticsSerializationDelegate delegate, {
2414-
bool fullDetails = true,
2415-
}) {
2416-
final Map<String, Object?> json = super.toJsonMap(
2417-
delegate,
2418-
fullDetails: fullDetails,
2419-
);
2420-
if (!fullDetails) {
2421-
return json;
2422-
}
2469+
Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
2470+
final Map<String, Object?> json = super.toJsonMap(delegate);
24232471
if (ifPresent != null) {
24242472
json['ifPresent'] = ifPresent;
24252473
}
@@ -2496,17 +2544,8 @@ class FlagsSummary<T> extends DiagnosticsProperty<Map<String, T?>> {
24962544
}
24972545

24982546
@override
2499-
Map<String, Object?> toJsonMap(
2500-
DiagnosticsSerializationDelegate delegate, {
2501-
bool fullDetails = true,
2502-
}) {
2503-
final Map<String, Object?> json = super.toJsonMap(
2504-
delegate,
2505-
fullDetails: fullDetails,
2506-
);
2507-
if (!fullDetails) {
2508-
return json;
2509-
}
2547+
Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
2548+
final Map<String, Object?> json = super.toJsonMap(delegate);
25102549
if (value.isNotEmpty) {
25112550
json['values'] = _formattedValues().toList();
25122551
}
@@ -2625,10 +2664,7 @@ class DiagnosticsProperty<T> extends DiagnosticsNode {
26252664
final bool allowNameWrap;
26262665

26272666
@override
2628-
Map<String, Object?> toJsonMap(
2629-
DiagnosticsSerializationDelegate delegate, {
2630-
bool fullDetails = true,
2631-
}) {
2667+
Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
26322668
final T? v = value;
26332669
List<Map<String, Object?>>? properties;
26342670
if (delegate.expandPropertyValues && delegate.includeProperties && v is Diagnosticable && getProperties().isEmpty) {
@@ -2638,16 +2674,9 @@ class DiagnosticsProperty<T> extends DiagnosticsNode {
26382674
delegate.filterProperties(v.toDiagnosticsNode().getProperties(), this),
26392675
this,
26402676
delegate,
2641-
fullDetails: fullDetails,
26422677
);
26432678
}
2644-
final Map<String, Object?> json = super.toJsonMap(
2645-
delegate,
2646-
fullDetails: fullDetails,
2647-
);
2648-
if (!fullDetails) {
2649-
return json;
2650-
}
2679+
final Map<String, Object?> json = super.toJsonMap(delegate);
26512680
if (properties != null) {
26522681
json['properties'] = properties;
26532682
}

packages/flutter/lib/src/painting/colors.dart

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -495,17 +495,8 @@ class ColorProperty extends DiagnosticsProperty<Color> {
495495
});
496496

497497
@override
498-
Map<String, Object?> toJsonMap(
499-
DiagnosticsSerializationDelegate delegate, {
500-
bool fullDetails = true,
501-
}) {
502-
final Map<String, Object?> json = super.toJsonMap(
503-
delegate,
504-
fullDetails: fullDetails,
505-
);
506-
if (!fullDetails) {
507-
return json;
508-
}
498+
Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
499+
final Map<String, Object?> json = super.toJsonMap(delegate);
509500
if (value != null) {
510501
json['valueProperties'] = <String, Object>{
511502
'red': value!.red,

0 commit comments

Comments
 (0)