Skip to content
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

Closed
wants to merge 1 commit into from
Closed

Run commit hooks before layout calculation #36216

wants to merge 1 commit into from

Conversation

tomekzaw
Copy link
Contributor

@tomekzaw tomekzaw commented Feb 20, 2023

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

Test Plan

The only UIManagerCommitHook I could find in the OSS repo is TimelineController.

@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Feb 20, 2023
@analysis-bot
Copy link

Platform Engine Arch Size (bytes) Diff
android hermes arm64-v8a 8,451,936 -17
android hermes armeabi-v7a 7,775,337 +4
android hermes x86 8,927,580 +18
android hermes x86_64 8,785,167 +2
android jsc arm64-v8a 6,668,733 -20
android jsc armeabi-v7a 7,462,722 +2
android jsc x86 9,192,890 +10
android jsc x86_64 6,893,883 +1

Base commit: 421df9f
Branch: main

@sammy-SC
Copy link
Contributor

Hello @tomekzaw,

Finally, this PR eliminates a crash on emitLayoutEvents(affectedLayoutableNodes); when commit hook actually modifies the tree and thus de-allocates old shadow nodes.

Do you have a repro for this? This sounds like something we should fix!

Moving the delegate_.shadowTreeWillCommit call to earlier might break the contact. If it is moved to earlier, commit can fail because another thread beats it to it. So we would call shadowTreeWillCommit knowing the commit still can fail. Do you think this can be a problem? Calling shadowTreeWillCommit knowing it may not be comitted sounds

operate on unsealed nodes which can make it more efficient in some cases.

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?

@tomekzaw
Copy link
Contributor Author

tomekzaw commented Feb 22, 2023

Hello @sammy-SC, thanks for your response!

Do you have a repro for this? This sounds like something we should fix!

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 <View /> component in a render. I've implemented and registered a UIManagerCommitHook that always makes some change in the tree, for instance changes the height of this view (and thus allocates a new ShadowNode) and returns the new RootShadowNode. When the transaction is committed, affectedLayoutableNodes will contain a raw pointer to the first clone (with changed width) which will unfortunately be deallocated once the hook makes a copy with changed height), and emitLayoutEvents will crash because of bad memory access.

Moving the delegate_.shadowTreeWillCommit call to earlier might break the contact. If it is moved to earlier, commit can fail because another thread beats it to it. So we would call shadowTreeWillCommit knowing the commit still can fail. Do you think this can be a problem? Calling shadowTreeWillCommit knowing it may not be comitted sounds

You're right, the name shadowTreeWillCommit suggests it is invoked immediately before the commit, however in reality the transaction may still fail. On the other hand, in the existing code there's already a call to telemetry.willCommit(); even before the transaction is executed. What if we rename this callback to shadowTreeWillLayout? This is exactly what I'm trying to achieve.

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.

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 ShadowNodeWrapper and can be used as a source ShadowNode when calling cloneWithNewProps or other variant.

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 completeRoot) because after the jump to the background thread, ShadowNodes can be still accessed from the JS thread, but it doesn't make much sense to do so I think.

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)?

Have you ran into any problems with the current implementation of commit hooks? What is the underlaying problem you are trying to solve?

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 emitLayoutEvents. As a workaround, we could store the oldRootShadowNode in some private field to prevent it from deallocating since we receive it as RootShadowNode::Shared const & but I don't think this should be a responsibility of a commit hook.

TL;DR I understand why it's safer for UIManagerCommitHook to operate on sealed nodes but I don't see the reason to calculate the layout before running commit hooks.

@facebook-github-bot
Copy link
Contributor

@sammy-SC has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

@tomekzaw
Copy link
Contributor Author

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 ShadowNode and one for layout calculation inside LayoutableShadowNode. What do you think?

@sammy-SC
Copy link
Contributor

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?

@tomekzaw
Copy link
Contributor Author

I started to import this PR but there are some internal CI failures (looks like they are unrelated to this change).

Great news, thanks!

Do you propose implementing two sealed flags in this PR before merging it?

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.

@sammy-SC
Copy link
Contributor

Ok, @tomekzaw sounds good.

@facebook-github-bot facebook-github-bot added the Merged This PR has been merged. label Feb 28, 2023
@facebook-github-bot
Copy link
Contributor

@sammy-SC merged this pull request in 8d0b5af.

@tomekzaw tomekzaw deleted the patch-1 branch February 28, 2023 09:45
OlimpiaZurek pushed a commit to OlimpiaZurek/react-native that referenced this pull request May 22, 2023
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
github-merge-queue bot pushed a commit to software-mansion/react-native-reanimated that referenced this pull request Jun 21, 2023
## 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
facebook-github-bot pushed a commit that referenced this pull request Aug 4, 2023
…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
lunaleaps pushed a commit that referenced this pull request Aug 8, 2023
…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
lunaleaps pushed a commit that referenced this pull request Aug 10, 2023
…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
Kudo pushed a commit to expo/react-native that referenced this pull request Aug 21, 2023
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Merged This PR has been merged.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants