Add cloneMultiple method to the ShadowNode class#50624
Add cloneMultiple method to the ShadowNode class#50624bartlomiejbloniarz wants to merge 8 commits into
cloneMultiple method to the ShadowNode class#50624Conversation
cloneMultiple method to the ShadowNode class
| const ShadowNode& oldShadowNode, | ||
| const std::optional<ShadowNode::ListOfShared>& newChildren)>& callback) | ||
| const { | ||
| std::unordered_map<const ShadowNodeFamily*, int> childrenCount; |
There was a problem hiding this comment.
| std::unordered_map<const ShadowNodeFamily*, int> childrenCount; | |
| std::unordered_map<const std::ref<const ShadowNodeFamily>, unsigned int> childrenCounts; |
|
|
||
| Unshared cloneMultipleRecursive( | ||
| const ShadowNode& shadowNode, | ||
| const std::unordered_set<const ShadowNodeFamily*>& families, |
There was a problem hiding this comment.
Let's pass ShadowNodeFamily as a std::ref instead of a pointer maybe?
| const std::unordered_set<const ShadowNodeFamily*>& families, | |
| const std::unordered_set<const ShadowNodeFamily &>& families, |
| const std::unordered_set<const ShadowNodeFamily*>& families, | ||
| const std::function<Unshared( | ||
| const ShadowNode& oldShadowNode, | ||
| const std::optional<ShadowNode::ListOfShared>& newChildren)>& |
There was a problem hiding this comment.
Let's also explain why newChildren is optional.
| */ | ||
| Unshared cloneMultiple( | ||
| const std::unordered_set<const ShadowNodeFamily*>& families, | ||
| const std::function<Unshared( |
There was a problem hiding this comment.
Let's also mention what happens if callback returns nullptr?
There was a problem hiding this comment.
The implementation is not really well defined if you call this method on anything that's not the RootShadowNode. Should this live on the root instead?
| } | ||
|
|
||
| if (childrenCount.empty()) { | ||
| return ShadowNode::Unshared{nullptr}; |
There was a problem hiding this comment.
Do we need to be that explicit about the return type?
| return ShadowNode::Unshared{nullptr}; | |
| return nullptr; |
javache
left a comment
There was a problem hiding this comment.
Since these methods don't actually need to live on ShadowNode, what about creating a ShadowNodeMutations helper class (and defining it as a friend class if necessary)
| const std::unordered_set<const ShadowNodeFamily*>& families, | ||
| const std::unordered_map<const ShadowNodeFamily*, int>& childrenCount, |
There was a problem hiding this comment.
Instead of using pointers to ShadowNodeFamily, let's try to use refs, eg using std::reference_wrapper
There was a problem hiding this comment.
I tried that. The problem with it is that I use unordered containers, so that would require implementing hash methods for the family.
For now I changed the loop over ancestors, to lock the weak pointer, so that when we access any fields of a family, we are sure it's still around. After that we only compare these pointers as numbers - we never dereference them so it will be safe. This would only fail if someone passed familiesToUpdate to the function, without owning them. In reanimated we keep shared_ptrs of ShadowNodes related to these families to ensure that they are still available when we perform the algorithm.
What do you think?
There was a problem hiding this comment.
We could change the api to instead work with shared_ptrs. The problem is that users of this api (so for example reanimated), don't have access to shared_ptrs of the ShadowNodeFamily class, as you only can get a reference from the ShadowNode.getFamily() method.
| const std::optional<ShadowNode::ListOfShared>& newChildren)>& callback) | ||
| const { | ||
| const auto family = &shadowNode.getFamily(); | ||
| auto children = shadowNode.getChildren(); |
There was a problem hiding this comment.
Avoid copying the list
| auto children = shadowNode.getChildren(); | |
| const auto& children = shadowNode.getChildren(); |
There was a problem hiding this comment.
I am copying that on purpose, as I want to modify it later on in the loop.
There was a problem hiding this comment.
You could also model this as a std::optional and only do the copy when you first need to. That also avoids the need for shouldUpdateChildren.
|
|
||
| ShadowNode::Unshared ShadowNode::cloneMultipleRecursive( | ||
| const ShadowNode& shadowNode, | ||
| const std::unordered_set<const ShadowNodeFamily*>& families, |
There was a problem hiding this comment.
Will rename
| const std::unordered_set<const ShadowNodeFamily*>& families, | ||
| const std::function<Unshared( | ||
| const ShadowNode& oldShadowNode, | ||
| const std::optional<ShadowNode::ListOfShared>& newChildren)>& callback) |
There was a problem hiding this comment.
Pass in a ShadowNodeFragment instead of children explicitly
|
@javache Sure I can extract it to a separate file. Should I also move the |
|
@javache What is the state of this? Do you think this approach needs reworking? |
| } | ||
|
|
||
| ShadowNode::Unshared ShadowNode::cloneMultipleRecursive( | ||
| const ShadowNode& shadowNode, |
There was a problem hiding this comment.
Could this just be a instance method? Alternatively make it static if it doesn't depend on instance state.
| const std::function<Unshared( | ||
| const ShadowNode& oldShadowNode, | ||
| const ShadowNodeFragment& fragment)>& callback) const { | ||
| const auto family = &shadowNode.getFamily(); |
There was a problem hiding this comment.
Use const auto * to make it explicit this isn't a copy. Even better would be to just use the ref here, and only deref it when looking it up.
| const std::optional<ShadowNode::ListOfShared>& newChildren)>& callback) | ||
| const { | ||
| const auto family = &shadowNode.getFamily(); | ||
| auto children = shadowNode.getChildren(); |
There was a problem hiding this comment.
You could also model this as a std::optional and only do the copy when you first need to. That also avoids the need for shouldUpdateChildren.
| */ | ||
| Unshared cloneMultiple( | ||
| const std::unordered_set<const ShadowNodeFamily*>& families, | ||
| const std::function<Unshared( |
There was a problem hiding this comment.
The implementation is not really well defined if you call this method on anything that's not the RootShadowNode. Should this live on the root instead?
Yeah, that seems reasonable. Let's keep it backwards compatible though and forward the API. |
|
@javache has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
|
This pull request was successfully merged by @bartlomiejbloniarz in 1161fb4 When will my fix make it into a release? | How to file a pick request? |
Summary:
This PR adds a new cloning method, allowing for updating multiple nodes in a single transaction. It works in two phases:
So the idea is that when we want to update all the red nodes in this picture, we first find the nodes in the green area and the clone only them in the correct order (children are cloned before parents):

Adapting this method brought a huge performance gain to reanimated. I want to upstream it, so that:
ShadowNodeclass gives us access to the parent field inShadowNodeFamilyso we can traverse the tree upwards, allowing for a optimal implementation of the first phase (in reanimated we repeatedly callgetAncestors, which revisits some nodes multiple times)A naive approach that calls
cloneTreefor every node is much slower, as it has to repeat many operations.Changelog:
[GENERAL] [ADDED] - Added
cloneMultipletoShadowNodeclass.Test Plan:
I tested it with the following reanimated implementation and everything works fine:
Details
I would like to add tests for it, but I'm not sure what's the best approach for that in the repo.