Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Optimize Shadow Tree cloning (#6214)
## Summary This PR optimizes the algorithm used to clone the Shadow Tree used in `ReanimatedCommitHook` and `performOperations`. ## Current approach The current algorithm works as follows. After receiving a batch of updates, we iterate over the list and apply the changes one by one. To apply changes we have to: 1. calculate the path from the affected ShadowNode to the root using `ShadowNodeFamily::getAncestors` 2. traverse the path upwards cloning all the nodes up to the root This way we unfortunately clone some ShadowNodes multiple times. For example for a batch of size `n` we will clone the root node `n` times. Cloning ShadowNodes is expensive, so we had implemented an optimization - whenever a node is unsealed we would change it in place instead of cloning it. Unfortunately this didn't work, since the `getSealed` method always returns `true` in Production mode. This is not a bug, but the intended behavior, as sealing is only intended to help finding bugs in the Debug Mode. This still could be salvaged by memoizing which nodes were already cloned by us, but this approach still wouldn't be perfect, as modyfing nodes in place is still a heavy operation. ## New approach To mitigate those issues we split the process into two phases: 1. calculate the subtree of the ShadowTree that contains all the nodes that we want to update 2. traverse the ShadowTree and clone nodes (that belong to the subtree) in the (reversed) topological order By calculating the subtree first we ensure that in the second phase: 1. we traverse only nodes that absolutely have to be traversed 2. we clone only nodes that absolutely have to be cloned 3. we clone every node at most once With this approach the second phase is performed in the optimal number of operations. ## Limitations The current implementation of phase one (building the subtree) is not optimal. It is implemented by simply calling `getAncestors` on every node from the batch. This is fortunately not a huge problem, because cloning had a much heavier impact on the performance. To optimize this there will have to be some changes done in RN (because the `parent` field in `ShadowNodeFamily` is private, so traversing the tree upwards is only possible through `getAncestors`). I hope to soon open a suitable PR. ## Some examples I checked the performance of our heavier examples on some devices in the Release Mode. For the `BokehExample.tsx` the results are: | Phone | Example size | Before [FPS] | After [FPS] | | -------- | ------- | -------- | ------- | | iPhone 12 mini | 200 | 30-40 | 60 | | One+ A6 | 100 | 10-20 | 30-40 | | iPhone 15 Pro | 250 | 30-40 | 120 | | Samsung Galaxy S23 | 100 | 55-70 | 120 | I also tested through Xcode Instruments how much time does the `performOperations` function take on the same example. Tests were conducted on the iPhone simulator, but they should give an idea on the order of the number of operations this function makes (and how fast that number grows in relation to the example size). | Example size | Before [ms] | After [ms] | Before/After | | ------- | -------- | ------- | ------- | | 1 | 1.95 | 2.1 | 0.92 | | 20 | 2.4 | 2.1 | 1.14 | | 100 | 5.3 | 2.3 | 2.3 | | 250 | 22 | 4 | 5.5 | | 500 | 77 | 7.7 | 10 | ## Test plan Check the behavior of examples in the `FabricExample` app. Verify that heavy examples have improved, while simpler examples have not regressed.
- Loading branch information