Fix deadlock in AnimationBackendChoreographer#56305
Open
bartlomiejbloniarz wants to merge 1 commit intomainfrom
Open
Fix deadlock in AnimationBackendChoreographer#56305bartlomiejbloniarz wants to merge 1 commit intomainfrom
bartlomiejbloniarz wants to merge 1 commit intomainfrom
Conversation
Summary:
scheduleCallback() and pause() held synchronized(paused) while calling
ReactChoreographer.postFrameCallback/removeFrameCallback, which acquire
synchronized(callbackQueues). ReactChoreographer dispatches frame
callbacks inside synchronized(callbackQueues), invoking
executeFrameCallback -> scheduleCallback -> synchronized(paused).
This is a lock ordering inversion: background thread acquires paused
then callbackQueues, main thread acquires callbackQueues then paused.
Fix: move postFrameCallback/removeFrameCallback calls outside the
synchronized(paused) block. The atomic state check (paused +
callbackPosted) stays inside the lock, only the ReactChoreographer
interaction moves outside.
Why this is safe:
- callbackPosted.getAndSet(true) inside the lock guarantees at most one
thread proceeds to post. A concurrent scheduleCallback will see
callbackPosted=true and bail out.
- If pause() runs between the lock release and postFrameCallback:
pause sets paused=true and callbackPosted=false, then calls
removeFrameCallback. Two outcomes depending on ordering inside
callbackQueues (which serializes both calls):
(a) post runs first, then remove cancels it — clean.
(b) remove runs first (nothing to remove), then post adds a callback.
That callback fires once, executeFrameCallback sees paused=true
via scheduleCallback and does not re-post. Net effect: one extra
no-op frame, then the loop stops.
- resume() already operated without synchronized(paused) before this
change, so no new races are introduced on that path.
Differential Revision: D99099455
|
@bartlomiejbloniarz has exported this pull request. If you are a Meta employee, you can view the originating Diff in D99099455. |
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:
scheduleCallback() and pause() held synchronized(paused) while calling
ReactChoreographer.postFrameCallback/removeFrameCallback, which acquire
synchronized(callbackQueues). ReactChoreographer dispatches frame
callbacks inside synchronized(callbackQueues), invoking
executeFrameCallback -> scheduleCallback -> synchronized(paused).
This is a lock ordering inversion: background thread acquires paused
then callbackQueues, main thread acquires callbackQueues then paused.
Fix: move postFrameCallback/removeFrameCallback calls outside the
synchronized(paused) block. The atomic state check (paused +
callbackPosted) stays inside the lock, only the ReactChoreographer
interaction moves outside.
Why this is safe:
thread proceeds to post. A concurrent scheduleCallback will see
callbackPosted=true and bail out.
pause sets paused=true and callbackPosted=false, then calls
removeFrameCallback. Two outcomes depending on ordering inside
callbackQueues (which serializes both calls):
(a) post runs first, then remove cancels it — clean.
(b) remove runs first (nothing to remove), then post adds a callback.
That callback fires once, executeFrameCallback sees paused=true
via scheduleCallback and does not re-post. Net effect: one extra
no-op frame, then the loop stops.
change, so no new races are introduced on that path.
Differential Revision: D99099455