Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit b0a1649

Browse files
committed
Rewrite the MultiChildRenderObjectWrapper syncing algorithm.
This also changes the way we insert nodes into a MultiChildRenderObjectWrapper's renderObject, which fixes issue #626. Now, instead of the slot being a renderObject, it's the Widget that currently uses that renderObject. That way when the Widget changes which renderObject to use, the siblings of that Widget in the same child list don't have to be notified of the change. I tested performance of the new algorithm vs the old algorithm using the stocks demo at idle and the stocks demo scrolling steadily. The data suggests the algorithms are roughly equivalent in performance.
1 parent 1f92cf5 commit b0a1649

File tree

3 files changed

+224
-113
lines changed

3 files changed

+224
-113
lines changed

sky/packages/sky/lib/widgets/framework.dart

Lines changed: 131 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export 'package:sky/rendering/box.dart' show BoxConstraints, BoxDecoration, Bord
1919
export 'package:sky/rendering/flex.dart' show FlexDirection;
2020
export 'package:sky/rendering/object.dart' show Point, Offset, Size, Rect, Color, Paint, Path;
2121

22-
final bool _shouldLogRenderDuration = false;
22+
final bool _shouldLogRenderDuration = false; // see also 'enableProfilingLoop' argument to runApp()
2323

2424
typedef Widget Builder();
2525
typedef void WidgetTreeWalker(Widget);
@@ -324,7 +324,7 @@ abstract class Widget {
324324
}
325325

326326
if (oldNode != null) {
327-
if (oldNode.runtimeType == newNode.runtimeType && oldNode.key == newNode.key) {
327+
if (_canSync(newNode, oldNode)) {
328328
if (oldNode.retainStatefulNodeIfPossible(newNode)) {
329329
assert(oldNode.mounted);
330330
assert(!newNode.mounted);
@@ -398,6 +398,10 @@ abstract class Widget {
398398
}
399399
}
400400

401+
bool _canSync(Widget a, Widget b) {
402+
return a.runtimeType == b.runtimeType && a.key == b.key;
403+
}
404+
401405

402406
// Descendants of TagNode provide a way to tag RenderObjectWrapper and
403407
// Component nodes with annotations, such as event listeners,
@@ -707,8 +711,7 @@ abstract class StatefulComponent extends Component {
707711
bool retainStatefulNodeIfPossible(StatefulComponent newNode) {
708712
assert(!_disqualifiedFromEverAppearingAgain);
709713
assert(newNode != null);
710-
assert(runtimeType == newNode.runtimeType);
711-
assert(key == newNode.key);
714+
assert(_canSync(this, newNode);
712715
assert(_child != null);
713716
newNode._disqualifiedFromEverAppearingAgain = true;
714717

@@ -894,7 +897,7 @@ abstract class RenderObjectWrapper extends Widget {
894897
if (_ancestor is RenderObjectWrapper)
895898
_ancestor.insertChildRenderObject(this, slot);
896899
} else {
897-
assert(old.runtimeType == runtimeType);
900+
assert(_canSync(this, old));
898901
_renderObject = old.renderObject;
899902
_ancestor = old._ancestor;
900903
assert(_renderObject != null);
@@ -1007,8 +1010,9 @@ abstract class OneChildRenderObjectWrapper extends RenderObjectWrapper {
10071010

10081011
abstract class MultiChildRenderObjectWrapper extends RenderObjectWrapper {
10091012

1010-
// In MultiChildRenderObjectWrapper subclasses, slots are RenderObject nodes
1011-
// to use as the "insert before" sibling in ContainerRenderObjectMixin.add() calls
1013+
// In MultiChildRenderObjectWrapper subclasses, slots are the Widget
1014+
// nodes whose RenderObjects are to be used as the "insert before"
1015+
// sibling in ContainerRenderObjectMixin.add() calls
10121016

10131017
MultiChildRenderObjectWrapper({ Key key, List<Widget> children })
10141018
: this.children = children == null ? const [] : children,
@@ -1023,11 +1027,12 @@ abstract class MultiChildRenderObjectWrapper extends RenderObjectWrapper {
10231027
walker(child);
10241028
}
10251029

1026-
void insertChildRenderObject(RenderObjectWrapper child, dynamic slot) {
1030+
void insertChildRenderObject(RenderObjectWrapper child, Widget slot) {
10271031
final renderObject = this.renderObject; // TODO(ianh): Remove this once the analyzer is cleverer
1028-
assert(slot == null || slot is RenderObject);
1032+
RenderObject nextSibling = slot != null ? slot.renderObject : null;
1033+
assert(nextSibling == null || nextSibling is RenderObject);
10291034
assert(renderObject is ContainerRenderObjectMixin);
1030-
renderObject.add(child.renderObject, before: slot);
1035+
renderObject.add(child.renderObject, before: nextSibling);
10311036
assert(renderObject == this.renderObject); // TODO(ianh): Remove this once the analyzer is cleverer
10321037
}
10331038

@@ -1060,121 +1065,134 @@ abstract class MultiChildRenderObjectWrapper extends RenderObjectWrapper {
10601065
assert(renderObject is ContainerRenderObjectMixin);
10611066
assert(old == null || old.renderObject == renderObject);
10621067

1063-
var startIndex = 0;
1064-
var endIndex = children.length;
1065-
1066-
var oldChildren = old == null ? [] : old.children;
1067-
var oldStartIndex = 0;
1068-
var oldEndIndex = oldChildren.length;
1069-
1070-
RenderObject nextSibling = null;
1071-
Widget currentNode = null;
1072-
Widget oldNode = null;
1073-
1074-
void sync(int atIndex) {
1075-
children[atIndex] = syncChild(currentNode, oldNode, nextSibling);
1076-
assert(children[atIndex] != null);
1077-
assert(children[atIndex].parent == this);
1078-
if (atIndex > 0)
1079-
children[atIndex-1].updateSlot(children[atIndex].renderObject);
1080-
}
1081-
1082-
// Scan backwards from end of list while nodes can be directly synced
1083-
// without reordering.
1084-
while (endIndex > startIndex && oldEndIndex > oldStartIndex) {
1085-
currentNode = children[endIndex - 1];
1086-
oldNode = oldChildren[oldEndIndex - 1];
1087-
1088-
if (currentNode.runtimeType != oldNode.runtimeType || currentNode.key != oldNode.key) {
1068+
// This attempts to diff the new child list (this.children) with
1069+
// the old child list (old.children), and update our renderObject
1070+
// accordingly.
1071+
1072+
// The cases it tries to optimise for are:
1073+
// - the old list is empty
1074+
// - the lists are identical
1075+
// - there is an insertion or removal of one or more widgets in
1076+
// only one place in the list
1077+
// If a widget with a key is in both lists, it will be synced.
1078+
// Widgets without keys might be synced but there is no guarantee.
1079+
1080+
// The general approach is to sync the entire new list backwards, as follows:
1081+
// 1. Walk the lists from the top until you no longer have
1082+
// matching nodes. We don't sync these yet, but we now know to
1083+
// skip them below. We do this because at each sync we need to
1084+
// pass the pointer to the new next widget as the slot, which
1085+
// we can't do until we've synced the next child.
1086+
// 2. Walk the lists from the bottom, syncing nodes, until you no
1087+
// longer have matching nodes.
1088+
// At this point we narrowed the old and new lists to the point
1089+
// where the nodes no longer match.
1090+
// 3. Walk the narrowed part of the old list to get the list of
1091+
// keys and sync null with non-keyed items.
1092+
// 4. Walk the narrowed part of the new list backwards:
1093+
// * Sync unkeyed items with null
1094+
// * Sync keyed items with the source if it exists, else with null.
1095+
// 5. Walk the top list again but backwards, syncing the nodes.
1096+
// 6. Sync null with any items in the list of keys that are still
1097+
// mounted.
1098+
1099+
final List<Widget> newChildren = children;
1100+
final List<Widget> oldChildren = old == null ? const <Widget>[] : old.children;
1101+
int childrenTop = 0;
1102+
int newChildrenBottom = newChildren.length-1;
1103+
int oldChildrenBottom = oldChildren.length-1;
1104+
1105+
// top of the lists
1106+
while ((childrenTop <= oldChildrenBottom) && (childrenTop <= newChildrenBottom)) {
1107+
Widget oldChild = oldChildren[childrenTop];
1108+
assert(oldChild.mounted);
1109+
Widget newChild = newChildren[childrenTop];
1110+
assert(newChild == oldChild || !newChild.mounted);
1111+
if (!_canSync(oldChild, newChild))
10891112
break;
1090-
}
1091-
1092-
endIndex--;
1093-
oldEndIndex--;
1094-
sync(endIndex);
1095-
nextSibling = children[endIndex].renderObject;
1096-
}
1097-
1098-
HashMap<Key, Widget> oldNodeIdMap = null;
1099-
1100-
bool oldNodeReordered(Key key) {
1101-
return oldNodeIdMap != null &&
1102-
oldNodeIdMap.containsKey(key) &&
1103-
oldNodeIdMap[key] == null;
1113+
childrenTop += 1;
11041114
}
11051115

1106-
void advanceOldStartIndex() {
1107-
oldStartIndex++;
1108-
while (oldStartIndex < oldEndIndex &&
1109-
oldNodeReordered(oldChildren[oldStartIndex].key)) {
1110-
oldStartIndex++;
1111-
}
1112-
}
1116+
Widget nextSibling;
11131117

1114-
void ensureOldIdMap() {
1115-
if (oldNodeIdMap != null)
1116-
return;
1117-
1118-
oldNodeIdMap = new HashMap<Key, Widget>();
1119-
for (int i = oldStartIndex; i < oldEndIndex; i++) {
1120-
var node = oldChildren[i];
1121-
if (node.key != null)
1122-
oldNodeIdMap.putIfAbsent(node.key, () => node);
1123-
}
1118+
// bottom of the lists
1119+
while ((childrenTop <= oldChildrenBottom) && (childrenTop <= newChildrenBottom)) {
1120+
Widget oldChild = oldChildren[oldChildrenBottom];
1121+
assert(oldChild.mounted);
1122+
Widget newChild = newChildren[newChildrenBottom];
1123+
assert(newChild == oldChild || !newChild.mounted);
1124+
if (!_canSync(oldChild, newChild))
1125+
break;
1126+
newChild = syncChild(newChild, oldChild, nextSibling);
1127+
assert(newChild.mounted);
1128+
assert(oldChild == newChild || !oldChild.mounted);
1129+
newChildren[newChildrenBottom] = newChild;
1130+
nextSibling = newChild;
1131+
oldChildrenBottom -= 1;
1132+
newChildrenBottom -= 1;
11241133
}
11251134

1126-
void searchForOldNode() {
1127-
if (currentNode.key == null)
1128-
return; // never re-order these nodes
1129-
1130-
ensureOldIdMap();
1131-
oldNode = oldNodeIdMap[currentNode.key];
1132-
if (oldNode == null)
1133-
return;
1134-
1135-
oldNodeIdMap[currentNode.key] = null; // mark it reordered
1136-
assert(renderObject is ContainerRenderObjectMixin);
1137-
assert(old.renderObject is ContainerRenderObjectMixin);
1138-
assert(oldNode.renderObject != null);
1139-
1140-
renderObject.move(oldNode.renderObject, before: nextSibling);
1135+
// middle of the lists - old list
1136+
bool haveOldNodes = childrenTop <= oldChildrenBottom;
1137+
Map<Key, Widget> oldKeyedChildren;
1138+
if (haveOldNodes) {
1139+
oldKeyedChildren = new Map<Key, Widget>();
1140+
while (childrenTop <= oldChildrenBottom) {
1141+
Widget oldChild = oldChildren[oldChildrenBottom];
1142+
assert(oldChild.mounted);
1143+
if (oldChild.key != null) {
1144+
oldKeyedChildren[oldChild.key] = oldChild;
1145+
} else {
1146+
syncChild(null, oldChild, null);
1147+
}
1148+
oldChildrenBottom -= 1;
1149+
}
11411150
}
11421151

1143-
// Scan forwards, this time we may re-order;
1144-
nextSibling = renderObject.firstChild;
1145-
while (startIndex < endIndex && oldStartIndex < oldEndIndex) {
1146-
currentNode = children[startIndex];
1147-
oldNode = oldChildren[oldStartIndex];
1148-
1149-
if (currentNode.runtimeType == oldNode.runtimeType && currentNode.key == oldNode.key) {
1150-
nextSibling = renderObject.childAfter(nextSibling);
1151-
sync(startIndex);
1152-
startIndex++;
1153-
advanceOldStartIndex();
1154-
continue;
1152+
// middle of the lists - new list
1153+
while (childrenTop <= newChildrenBottom) {
1154+
Widget oldChild;
1155+
Widget newChild = newChildren[newChildrenBottom];
1156+
if (haveOldNodes) {
1157+
Key key = newChild.key;
1158+
if (key != null) {
1159+
oldChild = oldKeyedChildren[newChild.key];
1160+
if (oldChild != null) {
1161+
if (oldChild.runtimeType != newChild.runtimeType)
1162+
oldChild = null;
1163+
oldKeyedChildren.remove(key);
1164+
}
1165+
}
11551166
}
1156-
1157-
oldNode = null;
1158-
searchForOldNode();
1159-
sync(startIndex);
1160-
startIndex++;
1167+
assert(newChild == oldChild || !newChild.mounted);
1168+
newChild = syncChild(newChild, oldChild, nextSibling);
1169+
assert(newChild.mounted);
1170+
assert(oldChild == newChild || oldChild == null || !oldChild.mounted);
1171+
newChildren[newChildrenBottom] = newChild;
1172+
nextSibling = newChild;
1173+
newChildrenBottom -= 1;
11611174
}
1162-
1163-
// New insertions
1164-
oldNode = null;
1165-
while (startIndex < endIndex) {
1166-
currentNode = children[startIndex];
1167-
sync(startIndex);
1168-
startIndex++;
1175+
assert(oldChildrenBottom == newChildrenBottom);
1176+
assert(childrenTop == newChildrenBottom+1);
1177+
1178+
// now sync the top of the list
1179+
while (childrenTop > 0) {
1180+
childrenTop -= 1;
1181+
Widget oldChild = oldChildren[childrenTop];
1182+
assert(oldChild.mounted);
1183+
Widget newChild = newChildren[childrenTop];
1184+
assert(newChild == oldChild || !newChild.mounted);
1185+
assert(_canSync(oldChild, newChild));
1186+
newChild = syncChild(newChild, oldChild, nextSibling);
1187+
assert(newChild.mounted);
1188+
assert(oldChild == newChild || oldChild == null || !oldChild.mounted);
1189+
newChildren[childrenTop] = newChild;
1190+
nextSibling = newChild;
11691191
}
11701192

1171-
// Removals
1172-
currentNode = null;
1173-
while (oldStartIndex < oldEndIndex) {
1174-
oldNode = oldChildren[oldStartIndex];
1175-
syncChild(null, oldNode, null);
1176-
assert(oldNode.parent == null);
1177-
advanceOldStartIndex();
1193+
if (haveOldNodes && !oldKeyedChildren.isEmpty) {
1194+
for (Widget oldChild in oldKeyedChildren.values)
1195+
syncChild(null, oldChild, null);
11781196
}
11791197

11801198
assert(renderObject == this.renderObject); // TODO(ianh): Remove this once the analyzer is cleverer
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
TestRenderView enabled
2+
3+
PAINT FOR FRAME #1 ----------------------------------------------
4+
1 | TestPaintingCanvas() constructor: 800.0 x 600.0
5+
------------------------------------------------------------------------
6+
7+
PAINT FOR FRAME #2 ----------------------------------------------
8+
2 | TestPaintingCanvas() constructor: 800.0 x 600.0
9+
2 | paintChild RenderFlex at Point(0.0, 0.0)
10+
2 | | TestPaintingCanvas() constructor: 800.0 x 600.0
11+
2 | | paintChild RenderConstrainedBox at Point(350.0, 0.0)
12+
2 | | | TestPaintingCanvas() constructor: 800.0 x 600.0
13+
2 | | paintChild RenderConstrainedBox at Point(350.0, 100.0)
14+
2 | | | TestPaintingCanvas() constructor: 800.0 x 600.0
15+
------------------------------------------------------------------------
16+
17+
PAINT FOR FRAME #3 ----------------------------------------------
18+
3 | TestPaintingCanvas() constructor: 800.0 x 600.0
19+
3 | paintChild RenderFlex at Point(0.0, 0.0)
20+
3 | | TestPaintingCanvas() constructor: 800.0 x 600.0
21+
3 | | paintChild RenderConstrainedBox at Point(350.0, 0.0)
22+
3 | | | TestPaintingCanvas() constructor: 800.0 x 600.0
23+
3 | | paintChild RenderPadding at Point(390.0, 100.0)
24+
3 | | | TestPaintingCanvas() constructor: 800.0 x 600.0
25+
------------------------------------------------------------------------
26+
27+
PAINT FOR FRAME #4 ----------------------------------------------
28+
4 | TestPaintingCanvas() constructor: 800.0 x 600.0
29+
4 | paintChild RenderFlex at Point(0.0, 0.0)
30+
4 | | TestPaintingCanvas() constructor: 800.0 x 600.0
31+
4 | | paintChild RenderPadding at Point(390.0, 0.0)
32+
4 | | | TestPaintingCanvas() constructor: 800.0 x 600.0
33+
4 | | paintChild RenderPadding at Point(390.0, 20.0)
34+
4 | | | TestPaintingCanvas() constructor: 800.0 x 600.0
35+
------------------------------------------------------------------------
36+
PAINTED 4 FRAMES

0 commit comments

Comments
 (0)