-
Notifications
You must be signed in to change notification settings - Fork 24.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Run commit hooks before layout calculation #36216
Conversation
Base commit: 421df9f |
Hello @tomekzaw,
Do you have a repro for this? This sounds like something we should fix! Moving the
Is this what we want? Having sealed nodes guarantees thread safety and opening up mutation to a hook could lead to hard to reproduce bugs. The API explicitly asks the caller to return a new tree in case something needs to be changed. Have you ran into any problems with the current implementation of commit hooks? What is the underlaying problem you are trying to solve? |
Hello @sammy-SC, thanks for your response!
Well, kind of. I was able to consistently reproduce this crash in Reanimated's FabricExample app. I'm afraid that preparing a separate minimal reproducible example without react-native-reanimated would take plenty of time since it's not that easy to integrate with Fabric from the perspective of a third-party module. Instead, I can send you the exact commit in Reanimated where the issue occurs as well as include the instructions on how to setup the app so you can investigate the issue (at least on iOS). Alternatively, I can describe what I've done. Let's assume that my app only changes the width of a
You're right, the name
That's right, technically the current API forces the user to return a new RootShadowNode but obviously it can re-use the existing ShadowNodes. The difference here is that previously the ShadowNodes were already sealed when accessing them in a commit hook (so you would need to clone each ShadowNode along with the path to the root to make any changes) and after this change they would be still unsealed. This would make it possible to modify the original ShadowNode which is still kept in React.js FiberNodes as Also, commit hooks are executed synchronously on the same thread (usually the background thread) so this change shouldn't introduce any new problems in terms of thread safety. Technically, the whole ShadowTree should be sealed even earlier while still on the JS thread (e.g. in One solution might be to seal the nodes first, then let the commit hooks run and finally calculate the layout, but I don't it's currently possible to calculate layout for a sealed ShadowNode. Maybe we should have two separate flags for sealing, one for ShadowNode updates (props, children and state) and one for layout calculation (Yoga node)?
The current implementation of runs commit hooks after layout calculation, so if my commit hook makes some layout-related changes to the tree, I need to recalculate the layout again. It makes no sense to calculate the layout twice since the first calculation is effectively redundant. Ideally, we should calculate the layout only once on the final ShadowTree after all commit hooks are executed. While the above is just a minor inconvenience (it's pretty bad in terms of performance though when the commit happens on the UI thread, as it does in our case), the real problem here is the crash on TL;DR I understand why it's safer for |
@sammy-SC has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
Hey @sammy-SC, any update on this PR? My idea was to seal the nodes before running commit hooks and then calculate layout, but once the nodes are sealed we can no longer calculate layout without cloning the whole tree. This problem can be solved by having two sealed flags – one for props, children and state inside |
I started to import this PR but there are some internal CI failures (looks like they are unrelated to this change). Do you propose implementing two sealed flags in this PR before merging it? |
Great news, thanks!
The approach with two sealed flags should do the job here but I'm afraid it requires too much changes in the existing code and can be confusing in the future. If possible, let's leave this PR as it is in its current state. |
Ok, @tomekzaw sounds good. |
Summary: I've noticed that `UIManagerCommitHooks` are applied after calling `layoutIfNeeded`, meaning that if a commit hook makes some changes to the tree, it needs to calculate the layout again, effectively making the first calculation redundant. This PR swaps the order of these two operations and moves `shadowTreeWillCommit` call before `layoutIfNeeded`. Thanks to this change, commit hooks don't need to manually trigger layout calculations as well as can potentially operate on unsealed nodes which can make it more efficient in some cases. Finally, this PR eliminates a crash on `emitLayoutEvents(affectedLayoutableNodes);` when commit hook actually modifies the tree and thus de-allocates old shadow nodes. cc sammy-SC ## Changelog [GENERAL] [CHANGED] - Run commit hooks before layout calculation Pull Request resolved: facebook#36216 Test Plan: The only `UIManagerCommitHook` I could find in the OSS repo is [`TimelineController`](https://github.com/facebook/react-native/blob/8bd3edec88148d0ab1f225d2119435681fbbba33/ReactCommon/react/renderer/timeline/TimelineController.h#L26). Reviewed By: cipolleschi Differential Revision: D43569407 Pulled By: sammy-SC fbshipit-source-id: 9ab1de0954cac2eb00346be7af8c9b3572bd2c88
## Summary This draft PR introduces a new approach towards updating layout props on Fabric that uses `UIManagerCommitHook`. Previously, we would keep a registry of most recent ShadowNodes. Each time RN or Reanimated cloned a ShadowNode, we would retrieve the most recent version of it from the registry as well as store the new copy. Obviously, this required synchronization at the level of each `cloneNode` call so rendering large trees was slow. The new approach stores all most recent props of animated components in `PropsRegistry` and applies them on each RN render using `ReanimatedCommitHook` all at once which improves the performance significantly. Changes: * Removed `ReanimatedUIManagerBinding` * Added `ReanimatedCommitHook` * Converted `NewestShadowNodesRegistry` into `PropsRegistry` * Converted `_removeShadowNodeFromRegistry` into `_removeFromPropsRegistry` * Updated initialization flow Requires: * facebook/react-native#36216 (merged, thanks @sammy-SC, released in 0.72.0-rc.0) Things to consider: * Commit an identical tree and let `ReanimatedCommitHook` handle all updates instead of calling `isThereAnyLayoutProp`, enqueuing update in `operationsInBatch_` and updating `lastReanimatedRoot` but modifying only necessary ShadowNodes? * Copy `propsRegistry.map_` in `ReanimatedCommitHook` to prevent locking the UI thread while the background thread runs the commit hook? Detected problems: * If calculating layout of RN tree takes more than one frame, Reanimated will skip the commit before RN can commit its own tree and the problem still persists * If calculating layout of RN tree takes more than one frame, Reanimated will run the animations in the meantime and finally RN will mount outdated tree * When there's just a few layout props changes and many non-layout props changes (e.g. emoji shower and progress bar), we still apply all changes via commit (this is to enforce UI consistency). * On each RN render (even if only one component is changed) we need to apply all changes from the whole PropsRegistry (which contains all currently mounted animated components). ## Review Since this PR affects 31 files, it is recommended to review the commits one-by-one. ## Test plan Launch FabricExample and open the following examples: chessboard, width, ref
…ecution (#38715) Summary: Hello! This PR is a fix for one merged some time ago (#36216). In the PR check for `nullptr` value of `newRootShadowNode` just after performing commit hooks was overlooked. This PR restores previous behaviour of conditional commit cancellation after commit hook execution. ## Changelog: [INTERNAL] [FIXED] - Restore checking shadow tree commit cancellation after commit hook execution Pull Request resolved: #38715 Test Plan: Just register a commit hook that return `nullptr`. In that case current code crashes due to `nullptr` dereference. Reviewed By: sammy-SC Differential Revision: D47972245 Pulled By: ryancat fbshipit-source-id: 7599ad11ed4b2dcaf25e53f676ec4530e37410d5
…ecution (#38715) Summary: Hello! This PR is a fix for one merged some time ago (#36216). In the PR check for `nullptr` value of `newRootShadowNode` just after performing commit hooks was overlooked. This PR restores previous behaviour of conditional commit cancellation after commit hook execution. ## Changelog: [INTERNAL] [FIXED] - Restore checking shadow tree commit cancellation after commit hook execution Pull Request resolved: #38715 Test Plan: Just register a commit hook that return `nullptr`. In that case current code crashes due to `nullptr` dereference. Reviewed By: sammy-SC Differential Revision: D47972245 Pulled By: ryancat fbshipit-source-id: 7599ad11ed4b2dcaf25e53f676ec4530e37410d5
…ecution (#38715) Summary: Hello! This PR is a fix for one merged some time ago (#36216). In the PR check for `nullptr` value of `newRootShadowNode` just after performing commit hooks was overlooked. This PR restores previous behaviour of conditional commit cancellation after commit hook execution. ## Changelog: [INTERNAL] [FIXED] - Restore checking shadow tree commit cancellation after commit hook execution Pull Request resolved: #38715 Test Plan: Just register a commit hook that return `nullptr`. In that case current code crashes due to `nullptr` dereference. Reviewed By: sammy-SC Differential Revision: D47972245 Pulled By: ryancat fbshipit-source-id: 7599ad11ed4b2dcaf25e53f676ec4530e37410d5
…ecution (facebook#38715) Summary: Hello! This PR is a fix for one merged some time ago (facebook#36216). In the PR check for `nullptr` value of `newRootShadowNode` just after performing commit hooks was overlooked. This PR restores previous behaviour of conditional commit cancellation after commit hook execution. ## Changelog: [INTERNAL] [FIXED] - Restore checking shadow tree commit cancellation after commit hook execution Pull Request resolved: facebook#38715 Test Plan: Just register a commit hook that return `nullptr`. In that case current code crashes due to `nullptr` dereference. Reviewed By: sammy-SC Differential Revision: D47972245 Pulled By: ryancat fbshipit-source-id: 7599ad11ed4b2dcaf25e53f676ec4530e37410d5
Summary
I've noticed that
UIManagerCommitHooks
are applied after callinglayoutIfNeeded
, meaning that if a commit hook makes some changes to the tree, it needs to calculate the layout again, effectively making the first calculation redundant.This PR swaps the order of these two operations and moves
shadowTreeWillCommit
call beforelayoutIfNeeded
. Thanks to this change, commit hooks don't need to manually trigger layout calculations as well as can potentially operate on unsealed nodes which can make it more efficient in some cases.Finally, this PR eliminates a crash on
emitLayoutEvents(affectedLayoutableNodes);
when commit hook actually modifies the tree and thus de-allocates old shadow nodes.cc @sammy-SC
Changelog
[GENERAL] [CHANGED] - Run commit hooks before layout calculation
Test Plan
The only
UIManagerCommitHook
I could find in the OSS repo isTimelineController
.