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

Commit 320c27c

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 f2faa74 commit 320c27c

1 file changed

Lines changed: 128 additions & 109 deletions

File tree

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

Lines changed: 128 additions & 109 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 = true; // see also 'enableProfilingLoop' argument to runApp()
2323

2424
typedef Widget Builder();
2525
typedef void WidgetTreeWalker(Widget);
@@ -1007,8 +1007,9 @@ abstract class OneChildRenderObjectWrapper extends RenderObjectWrapper {
10071007

10081008
abstract class MultiChildRenderObjectWrapper extends RenderObjectWrapper {
10091009

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

10131014
MultiChildRenderObjectWrapper({ Key key, List<Widget> children })
10141015
: this.children = children == null ? const [] : children,
@@ -1023,11 +1024,12 @@ abstract class MultiChildRenderObjectWrapper extends RenderObjectWrapper {
10231024
walker(child);
10241025
}
10251026

1026-
void insertChildRenderObject(RenderObjectWrapper child, dynamic slot) {
1027+
void insertChildRenderObject(RenderObjectWrapper child, Widget slot) {
10271028
final renderObject = this.renderObject; // TODO(ianh): Remove this once the analyzer is cleverer
1028-
assert(slot == null || slot is RenderObject);
1029+
RenderObject nextSibling = slot is Widget ? slot.renderObject : null;
1030+
assert(nextSibling == null || nextSibling is RenderObject);
10291031
assert(renderObject is ContainerRenderObjectMixin);
1030-
renderObject.add(child.renderObject, before: slot);
1032+
renderObject.add(child.renderObject, before: nextSibling);
10311033
assert(renderObject == this.renderObject); // TODO(ianh): Remove this once the analyzer is cleverer
10321034
}
10331035

@@ -1053,128 +1055,145 @@ abstract class MultiChildRenderObjectWrapper extends RenderObjectWrapper {
10531055
return false;
10541056
}
10551057

1058+
bool _canSync(Widget a, Widget b) {
1059+
return (a.runtimeType == b.runtimeType) && (a.key == b.key);
1060+
}
1061+
10561062
void syncRenderObject(MultiChildRenderObjectWrapper old) {
10571063
super.syncRenderObject(old);
10581064

10591065
final ContainerRenderObjectMixin renderObject = this.renderObject; // TODO(ianh): Remove this once the analyzer is cleverer
10601066
assert(renderObject is ContainerRenderObjectMixin);
10611067
assert(old == null || old.renderObject == renderObject);
10621068

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

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

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-
}
1119+
// bottom of the lists
1120+
while ((childrenTop <= oldChildrenBottom) && (childrenTop <= newChildrenBottom)) {
1121+
Widget oldChild = oldChildren[oldChildrenBottom];
1122+
assert(oldChild.mounted);
1123+
Widget newChild = newChildren[newChildrenBottom];
1124+
assert(newChild == oldChild || !newChild.mounted);
1125+
if (!_canSync(oldChild, newChild))
1126+
break;
1127+
newChild = syncChild(newChild, oldChild, nextSibling);
1128+
assert(newChild.mounted);
1129+
assert(oldChild == newChild || !oldChild.mounted);
1130+
newChildren[newChildrenBottom] = newChild;
1131+
nextSibling = newChild;
1132+
oldChildrenBottom -= 1;
1133+
newChildrenBottom -= 1;
11241134
}
11251135

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);
1136+
// middle of the lists - old list
1137+
bool haveOldNodes = childrenTop <= oldChildrenBottom;
1138+
Map<Key, Widget> oldKeyedChildren;
1139+
if (haveOldNodes) {
1140+
oldKeyedChildren = new Map<Key, Widget>();
1141+
while (childrenTop <= oldChildrenBottom) {
1142+
Widget oldChild = oldChildren[oldChildrenBottom];
1143+
assert(oldChild.mounted);
1144+
if (oldChild.key != null) {
1145+
oldKeyedChildren[oldChild.key] = oldChild;
1146+
} else {
1147+
syncChild(null, oldChild, null);
1148+
}
1149+
oldChildrenBottom -= 1;
1150+
}
11411151
}
11421152

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;
1153+
// middle of the lists - new list
1154+
while (childrenTop <= newChildrenBottom) {
1155+
Widget oldChild;
1156+
Widget newChild = newChildren[newChildrenBottom];
1157+
if (haveOldNodes) {
1158+
Key key = newChild.key;
1159+
if (key != null) {
1160+
oldChild = oldKeyedChildren[newChild.key];
1161+
if (oldChild != null) {
1162+
if (oldChild.runtimeType != newChild.runtimeType)
1163+
oldChild = null;
1164+
oldKeyedChildren.remove(key);
1165+
}
1166+
}
11551167
}
1156-
1157-
oldNode = null;
1158-
searchForOldNode();
1159-
sync(startIndex);
1160-
startIndex++;
1168+
assert(newChild == oldChild || !newChild.mounted);
1169+
newChild = syncChild(newChild, oldChild, nextSibling);
1170+
assert(newChild.mounted);
1171+
assert(oldChild == newChild || oldChild == null || !oldChild.mounted);
1172+
newChildren[newChildrenBottom] = newChild;
1173+
nextSibling = newChild;
1174+
newChildrenBottom -= 1;
11611175
}
1162-
1163-
// New insertions
1164-
oldNode = null;
1165-
while (startIndex < endIndex) {
1166-
currentNode = children[startIndex];
1167-
sync(startIndex);
1168-
startIndex++;
1176+
assert(oldChildrenBottom == newChildrenBottom);
1177+
assert(childrenTop == newChildrenBottom+1);
1178+
1179+
// now sync the top of the list
1180+
while (childrenTop > 0) {
1181+
childrenTop -= 1;
1182+
Widget oldChild = oldChildren[childrenTop];
1183+
assert(oldChild.mounted);
1184+
Widget newChild = newChildren[childrenTop];
1185+
assert(newChild == oldChild || !newChild.mounted);
1186+
assert(_canSync(oldChild, newChild));
1187+
newChild = syncChild(newChild, oldChild, nextSibling);
1188+
assert(newChild.mounted);
1189+
assert(oldChild == newChild || oldChild == null || !oldChild.mounted);
1190+
newChildren[childrenTop] = newChild;
1191+
nextSibling = newChild;
11691192
}
11701193

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();
1194+
if (haveOldNodes && !oldKeyedChildren.isEmpty) {
1195+
for (Widget oldChild in oldKeyedChildren.values)
1196+
syncChild(null, oldChild, null);
11781197
}
11791198

11801199
assert(renderObject == this.renderObject); // TODO(ianh): Remove this once the analyzer is cleverer

0 commit comments

Comments
 (0)