fix: prevent exiting views from getting stuck under heavy load on Android#9621
Open
bartlomiejbloniarz wants to merge 4 commits into
Open
fix: prevent exiting views from getting stuck under heavy load on Android#9621bartlomiejbloniarz wants to merge 4 commits into
bartlomiejbloniarz wants to merge 4 commits into
Conversation
4b471ea to
28e533e
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes an Android-only race where deferred cleanup of finished layout animations could erase a newly re-created layoutAnimations_[tag] entry after tag reuse under UI-thread load, preventing exiting views from being marked dead and leaving them stuck on-screen. The change makes “settled” status derive from the per-tag in-flight animation count, and re-checks that status at cleanup/read sites to avoid wiping re-animated tags.
Changes:
- Replace “finished tag queue” cleanup with a “maybe-settled tag set” and only erase entries that are still settled (
count == 0) at cleanup time. - Update multiple read sites (legacy/experimental/shared transitions) to treat “settled” entries as effectively absent.
- Add an example app stress test to reproduce/tag-reuse under spiky load.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/SharedTransitions.cpp | Avoids using container tags backed by missing/settled layout-animation entries. |
| packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h | Introduces LayoutAnimation::isSettled() and replaces the finished-tag vector with an unordered_set. |
| packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.cpp | Updates opacity restoration logic to iterate the new “maybe settled” tag set. |
| packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp | Uses settled-aware cleanup and settled-aware checks across legacy layout animation handling. |
| packages/react-native-reanimated/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.cpp | Uses settled-aware cleanup and settled-aware checks across experimental layout animation handling. |
| apps/common-app/src/apps/reanimated/examples/LayoutAnimations/ExitingTagReuseStressExample.tsx | Adds a stress-test example to validate exiting-tag reuse under load. |
| apps/common-app/src/apps/reanimated/examples/index.ts | Registers the new example in the common app examples list. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…roid The deferred cleanup of finished layout animations could race with tag reuse and erase a freshly re-created entry, leaving the exiting view stuck on screen. Use the in-flight animation count as the source of truth for whether an entry has settled and re-check it before erasing.
28e533e to
a0153ee
Compare
After the cleanup loop in pullTransaction no entry can be settled (the mutex is held for the whole transaction, so no end callback runs and nothing creates a count==0 entry inline). The isSettled() guards at those read sites were dead code, so revert them to the original checks.
…ions, drop fake countdown
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #9170.
On Android an exiting
Animated.Viewcould stay stuck on screen after it was unmounted, under heavy/spiky UI-thread load.When a layout animation finishes, its entry in
layoutAnimations_isn't erased right away — the tag is queued and the entry is erased during the nextpullTransaction(so the final frame can still be flushed). That deferred erase races with tag reuse because the steps run on two different threads:endLayoutAnimationand thescheduleOnUI'dcreateLayoutAnimationrun on the UI thread, whilepullTransaction(and the cleanup at the top of it) runs on the mounting thread — which on Android is usually the JS thread, during React commits. The recursive mutex serializes each critical section but doesn't order them, and the re-create isn't done insidepullTransaction, so under load:[UI thread]endLayoutAnimation(T)— animation ends,Tis queued for cleanup.[UI thread]startExitingAnimation's scheduled job runscreateLayoutAnimation(T)—layoutAnimations_[T]is re-created for the reused tag.[JS thread]pullTransactionruns the cleanup, which erases the queued tags → wipes the freshly re-createdlayoutAnimations_[T].[UI thread]endLayoutAnimation(T, shouldRemove=true)—layoutAnimations_no longer containsT, so it returns early, the node is never markedDEAD, and the view stays on screen forever.On iOS
pullTransactionalways runs on the UI thread, so it's all one thread.scheduleOnUIcalled frompullTransactionrunscreateLayoutAnimationinline, after the cleanup, within the samepullTransaction— so step 2 can't slip in before step 3, and the window never opens.Ideally, once pull model / branching is made default we can remove the threading weridness around this.
The fix makes
count(the number of in-flight animations for a view) the source of truth for whether an entry has settled:endLayoutAnimationdecrements it to 0, and the cleanup re-checks it before erasing, so a tag that was re-animated in the meantime is back atcount >= 1(the start paths docount + 1) and is left alone. Read sites that can run before the cleanup in a transaction now also check that the entry isn't settled; sites that run after it don't need to — the cleanup holds the mutex for the whole transaction, so no settled entry can exist there.Test plan
Added an
[LA] Exiting tag reuse stressexample: a fixed grid that mounts/unmounts views in the same slots withZoomIn/ZoomOut, plus periodic spikes of short-lived views to load the UI thread.ENABLE_SHARED_ELEMENT_TRANSITIONSboth off (Legacy proxy) and on (Experimental proxy).