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

Commit 20e515e

Browse files
committed
Merge pull request #724 from Hixie/issue-626
Rewrite the MultiChildRenderObjectWrapper syncing algorithm.
2 parents 1f92cf5 + b0a1649 commit 20e515e

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)