Permalink
Browse files

Persistent Yoga

Summary:
This is meant to show a possible route format for a persistent form of Yoga. Where previous layouts can remain intact while still taking advantage of incremental layout by reusing previous subtrees.

```c
YGNodeRef YGNodeClone(const YGNodeRef node);
```

The core of this functionality is a new API to clone an existing node. This makes a new detached node with all the same values as the previous one. Conceptually this makes the original node "frozen" from that point on. It's now immutable. (This is not yet enforced at runtime in this PR but something we should add.)

Since the original is frozen, we reuse the children set from the original node. Their parent pointers still point back to the original tree though.

The cloned node is still mutable. It can have its styles updated, and nodes can be inserted or deleted. If an insertion/deletion happens on a cloned node whose children were reused, it'll first shallow clone its children automatically.

As a convenience I also added an API to clear all children:

```c
void YGNodeRemoveAllChildren(const YGNodeRef node);
```

During insert/delete, or as a result of layout a set of reused children may need to be first cloned. A kind of copy-on-write. When that happens, the host may want to respond. E.g. by updating the `context` such as by cloning any wrapper objects and attaching them to the new node.

```c
typedef void (*YGNodeClonedFunc)(YGNodeRef oldNode,
                                 YGNodeRef newNode,
                                 YGNodeRef parent,
                                 int childIndex);

void YGConfigSetNodeClonedFunc(YGConfigRef config,
                               YGNodeClonedFunc callback);
```

This PR doesn't change any existing semantics for trees that are not first cloned.

It's possible for a single node to exist in two trees at once and be used by multiple threads. Therefore it's not safe to recursively free a whole tree when you use persistence. To solve this, any user of the library has to manually manage ref counting or tracing GC. E.g. by replicating the tree structure in a wrapper.

In a follow up we could consider moving ref counting into Yoga.
Closes facebook/yoga#636

Reviewed By: emilsjolander

Differential Revision: D5941921

Pulled By: sebmarkbage

fbshipit-source-id: c8e93421824c112d09c4773bed4e3141b6491ccf
  • Loading branch information...
sebmarkbage authored and facebook-github-bot committed Oct 17, 2017
1 parent 0a7d5ab commit 60c898d8643daf80ef8175af7dc8411709eff379
@@ -7,6 +7,8 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/
#include <string.h>
#include "YGNodeList.h"
extern YGMalloc gYGMalloc;
@@ -72,6 +74,17 @@ void YGNodeListInsert(YGNodeListRef *listp, const YGNodeRef node, const uint32_t
list->items[index] = node;
}
void YGNodeListReplace(YGNodeListRef list, const uint32_t index, const YGNodeRef newNode) {
list->items[index] = newNode;
}
void YGNodeListRemoveAll(const YGNodeListRef list) {
for (uint32_t i = 0; i < list->count; i++) {
list->items[i] = NULL;
}
list->count = 0;
}
YGNodeRef YGNodeListRemove(const YGNodeListRef list, const uint32_t index) {
const YGNodeRef removed = list->items[index];
list->items[index] = NULL;
@@ -102,3 +115,17 @@ YGNodeRef YGNodeListGet(const YGNodeListRef list, const uint32_t index) {
return NULL;
}
YGNodeListRef YGNodeListClone(const YGNodeListRef oldList) {
if (!oldList) {
return NULL;
}
const uint32_t count = oldList->count;
if (count == 0) {
return NULL;
}
const YGNodeListRef newList = YGNodeListNew(count);
memcpy(newList->items, oldList->items, sizeof(YGNodeRef) * count);
newList->count = count;
return newList;
}
@@ -26,8 +26,11 @@ void YGNodeListFree(const YGNodeListRef list);
uint32_t YGNodeListCount(const YGNodeListRef list);
void YGNodeListAdd(YGNodeListRef *listp, const YGNodeRef node);
void YGNodeListInsert(YGNodeListRef *listp, const YGNodeRef node, const uint32_t index);
void YGNodeListReplace(const YGNodeListRef list, const uint32_t index, const YGNodeRef newNode);
void YGNodeListRemoveAll(const YGNodeListRef list);
YGNodeRef YGNodeListRemove(const YGNodeListRef list, const uint32_t index);
YGNodeRef YGNodeListDelete(const YGNodeListRef list, const YGNodeRef node);
YGNodeRef YGNodeListGet(const YGNodeListRef list, const uint32_t index);
YGNodeListRef YGNodeListClone(YGNodeListRef list);
YG_EXTERN_C_END
@@ -102,6 +102,7 @@ typedef struct YGConfig {
bool useLegacyStretchBehaviour;
float pointScaleFactor;
YGLogger logger;
YGNodeClonedFunc cloneNodeCallback;
void *context;
} YGConfig;
@@ -362,6 +363,17 @@ YGNodeRef YGNodeNew(void) {
return YGNodeNewWithConfig(&gYGConfigDefaults);
}
YGNodeRef YGNodeClone(const YGNodeRef oldNode) {
const YGNodeRef node = gYGMalloc(sizeof(YGNode));
YGAssertWithConfig(oldNode->config, node != NULL, "Could not allocate memory for node");
gNodeInstanceCount++;
memcpy(node, oldNode, sizeof(YGNode));
node->children = YGNodeListClone(oldNode->children);
node->parent = NULL;
return node;
}
void YGNodeFree(const YGNodeRef node) {
if (node->parent) {
YGNodeListDelete(node->parent->children, node);
@@ -382,6 +394,10 @@ void YGNodeFree(const YGNodeRef node) {
void YGNodeFreeRecursive(const YGNodeRef root) {
while (YGNodeGetChildCount(root) > 0) {
const YGNodeRef child = YGNodeGetChild(root, 0);
if (child->parent != root) {
// Don't free shared nodes that we don't own.
break;
}
YGNodeRemoveChild(root, child);
YGNodeFreeRecursive(child);
}
@@ -474,6 +490,34 @@ YGBaselineFunc YGNodeGetBaselineFunc(const YGNodeRef node) {
return node->baseline;
}
static void YGCloneChildrenIfNeeded(const YGNodeRef parent) {
// YGNodeRemoveChild has a forked variant of this algorithm optimized for deletions.
const uint32_t childCount = YGNodeGetChildCount(parent);
if (childCount == 0) {
// This is an empty set. Nothing to clone.
return;
}
const YGNodeRef firstChild = YGNodeGetChild(parent, 0);
if (firstChild->parent == parent) {
// If the first child has this node as its parent, we assume that it is already unique.
// We can do this because if we have it has a child, that means that its parent was at some
// point cloned which made that subtree immutable.
// We also assume that all its sibling are cloned as well.
return;
}
const YGNodeClonedFunc cloneNodeCallback = parent->config->cloneNodeCallback;
const YGNodeListRef children = parent->children;
for (uint32_t i = 0; i < childCount; i++) {
const YGNodeRef oldChild = YGNodeListGet(children, i);
const YGNodeRef newChild = YGNodeClone(oldChild);
YGNodeListReplace(children, i, newChild);
newChild->parent = parent;
if (cloneNodeCallback) {
cloneNodeCallback(oldChild, newChild, parent, i);
}
}
}
void YGNodeInsertChild(const YGNodeRef node, const YGNodeRef child, const uint32_t index) {
YGAssertWithNode(node,
child->parent == NULL,
@@ -482,19 +526,83 @@ void YGNodeInsertChild(const YGNodeRef node, const YGNodeRef child, const uint32
node->measure == NULL,
"Cannot add child: Nodes with measure functions cannot have children.");
YGCloneChildrenIfNeeded(node);
YGNodeListInsert(&node->children, child, index);
child->parent = node;
YGNodeMarkDirtyInternal(node);
}
void YGNodeRemoveChild(const YGNodeRef node, const YGNodeRef child) {
if (YGNodeListDelete(node->children, child) != NULL) {
child->layout = gYGNodeDefaults.layout; // layout is no longer valid
child->parent = NULL;
YGNodeMarkDirtyInternal(node);
void YGNodeRemoveChild(const YGNodeRef parent, const YGNodeRef excludedChild) {
// This algorithm is a forked variant from YGCloneChildrenIfNeeded that excludes a child.
const uint32_t childCount = YGNodeGetChildCount(parent);
if (childCount == 0) {
// This is an empty set. Nothing to remove.
return;
}
const YGNodeRef firstChild = YGNodeGetChild(parent, 0);
if (firstChild->parent == parent) {
// If the first child has this node as its parent, we assume that it is already unique.
// We can now try to delete a child in this list.
if (YGNodeListDelete(parent->children, excludedChild) != NULL) {
excludedChild->layout = gYGNodeDefaults.layout; // layout is no longer valid
excludedChild->parent = NULL;
YGNodeMarkDirtyInternal(parent);
}
return;
}
// Otherwise we have to clone the node list except for the child we're trying to delete.
// We don't want to simply clone all children, because then the host will need to free
// the clone of the child that was just deleted.
const YGNodeClonedFunc cloneNodeCallback = parent->config->cloneNodeCallback;
const YGNodeListRef children = parent->children;
uint32_t nextInsertIndex = 0;
for (uint32_t i = 0; i < childCount; i++) {
const YGNodeRef oldChild = YGNodeListGet(children, i);
if (excludedChild == oldChild) {
// Ignore the deleted child. Don't reset its layout or parent since it is still valid
// in the other parent. However, since this parent has now changed, we need to mark it
// as dirty.
YGNodeMarkDirtyInternal(parent);
continue;
}
const YGNodeRef newChild = YGNodeClone(oldChild);
YGNodeListReplace(children, nextInsertIndex, newChild);
newChild->parent = parent;
if (cloneNodeCallback) {
cloneNodeCallback(oldChild, newChild, parent, nextInsertIndex);
}
nextInsertIndex++;
}
while (nextInsertIndex < childCount) {
YGNodeListRemove(children, nextInsertIndex);
nextInsertIndex++;
}
}
void YGNodeRemoveAllChildren(const YGNodeRef parent) {
const uint32_t childCount = YGNodeGetChildCount(parent);
if (childCount == 0) {
// This is an empty set already. Nothing to do.
return;
}
const YGNodeRef firstChild = YGNodeGetChild(parent, 0);
if (firstChild->parent == parent) {
// If the first child has this node as its parent, we assume that this child set is unique.
for (uint32_t i = 0; i < childCount; i++) {
const YGNodeRef oldChild = YGNodeGetChild(parent, i);
oldChild->layout = gYGNodeDefaults.layout; // layout is no longer valid
oldChild->parent = NULL;
}
YGNodeListRemoveAll(parent->children);
YGNodeMarkDirtyInternal(parent);
return;
}
// Otherwise, we are not the owner of the child set. We don't have to do anything to clear it.
parent->children = NULL;
YGNodeMarkDirtyInternal(parent);
}
YGNodeRef YGNodeGetChild(const YGNodeRef node, const uint32_t index) {
return YGNodeListGet(node->children, index);
}
@@ -1909,6 +2017,7 @@ static bool YGNodeFixedSizeSetMeasuredDimensions(const YGNodeRef node,
static void YGZeroOutLayoutRecursivly(const YGNodeRef node) {
memset(&(node->layout), 0, sizeof(YGLayout));
node->hasNewLayout = true;
YGCloneChildrenIfNeeded(node);
const uint32_t childCount = YGNodeGetChildCount(node);
for (uint32_t i = 0; i < childCount; i++) {
const YGNodeRef child = YGNodeListGet(node->children, i);
@@ -2082,6 +2191,9 @@ static void YGNodelayoutImpl(const YGNodeRef node,
return;
}
// At this point we know we're going to perform work. Ensure that each child has a mutable copy.
YGCloneChildrenIfNeeded(node);
// Reset layout flags, as they could have changed.
node->layout.hadOverflow = false;
@@ -3698,6 +3810,10 @@ void *YGConfigGetContext(const YGConfigRef config) {
return config->context;
}
void YGConfigSetNodeClonedFunc(const YGConfigRef config, const YGNodeClonedFunc callback) {
config->cloneNodeCallback = callback;
}
void YGSetMemoryFuncs(YGMalloc ygmalloc, YGCalloc yccalloc, YGRealloc ygrealloc, YGFree ygfree) {
YGAssert(gNodeInstanceCount == 0 && gConfigInstanceCount == 0,
"Cannot set memory functions: all node must be freed first");
@@ -60,6 +60,10 @@ typedef int (*YGLogger)(const YGConfigRef config,
YGLogLevel level,
const char *format,
va_list args);
typedef void (*YGNodeClonedFunc)(YGNodeRef oldNode,
YGNodeRef newNode,
YGNodeRef parent,
int childIndex);
typedef void *(*YGMalloc)(size_t size);
typedef void *(*YGCalloc)(size_t count, size_t size);
@@ -69,6 +73,7 @@ typedef void (*YGFree)(void *ptr);
// YGNode
WIN_EXPORT YGNodeRef YGNodeNew(void);
WIN_EXPORT YGNodeRef YGNodeNewWithConfig(const YGConfigRef config);
WIN_EXPORT YGNodeRef YGNodeClone(const YGNodeRef node);
WIN_EXPORT void YGNodeFree(const YGNodeRef node);
WIN_EXPORT void YGNodeFreeRecursive(const YGNodeRef node);
WIN_EXPORT void YGNodeReset(const YGNodeRef node);
@@ -78,6 +83,7 @@ WIN_EXPORT void YGNodeInsertChild(const YGNodeRef node,
const YGNodeRef child,
const uint32_t index);
WIN_EXPORT void YGNodeRemoveChild(const YGNodeRef node, const YGNodeRef child);
WIN_EXPORT void YGNodeRemoveAllChildren(const YGNodeRef node);
WIN_EXPORT YGNodeRef YGNodeGetChild(const YGNodeRef node, const uint32_t index);
WIN_EXPORT YGNodeRef YGNodeGetParent(const YGNodeRef node);
WIN_EXPORT uint32_t YGNodeGetChildCount(const YGNodeRef node);
@@ -264,6 +270,9 @@ WIN_EXPORT bool YGConfigIsExperimentalFeatureEnabled(const YGConfigRef config,
WIN_EXPORT void YGConfigSetUseWebDefaults(const YGConfigRef config, const bool enabled);
WIN_EXPORT bool YGConfigGetUseWebDefaults(const YGConfigRef config);
WIN_EXPORT void YGConfigSetNodeClonedFunc(const YGConfigRef config,
const YGNodeClonedFunc callback);
// Export only for C#
WIN_EXPORT YGConfigRef YGConfigGetDefault(void);

0 comments on commit 60c898d

Please sign in to comment.