Skip to content

Commit cd09677

Browse files
Bartlomiej Bloniarzfacebook-github-bot
authored andcommitted
Fix render callback start/stop race in C++ NativeAnimated
Summary: The shared AnimationBackend path now coordinates render callback registration and publication with a small mutex-protected optional callback id. `startRenderCallbackIfNeeded` holds the lifecycle mutex across `AnimationBackend::start` and id publication because the backend registers before returning the id; `stopRenderCallbackIfNeeded` clears the published id under the same mutex before stopping that callback. This closes the window where a concurrent stop could observe no published id and leave the just-registered backend callback orphaned. The existing non-shared platform callback path remains on the original atomic flag, preserving the avoid-calling-external-code-under-a-mutex behavior. Changelog: [General][Fixed] - Fix a race in the C++ Animated render-callback lifecycle that could leave stale callbacks running on every frame Differential Revision: D109009181
1 parent ad2cf4d commit cd09677

2 files changed

Lines changed: 66 additions & 40 deletions

File tree

packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.cpp

Lines changed: 62 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -551,61 +551,84 @@ NativeAnimatedNodesManager::ensureEventEmitterListener() noexcept {
551551
}
552552

553553
void NativeAnimatedNodesManager::startRenderCallbackIfNeeded(bool isAsync) {
554-
// This method can be called from either the UI thread or JavaScript thread.
555-
// It ensures `startOnRenderCallback_` is called exactly once using atomic
556-
// operations. We use std::atomic_bool rather than std::mutex to avoid
557-
// potential deadlocks that could occur if we called external code while
558-
// holding a mutex.
559-
auto isRenderCallbackStarted = isRenderCallbackStarted_.exchange(true);
560-
if (isRenderCallbackStarted) {
561-
// onRender callback is already started.
562-
return;
563-
}
564-
565-
if (useSharedAnimatedBackend_) {
566-
if (auto animationBackend = animationBackend_.lock()) {
567-
auto weak = weak_from_this();
568-
animationBackendCallbackId_ = animationBackend->start(
569-
[weak](AnimationTimestamp timestamp) -> AnimationMutations {
570-
if (auto self = weak.lock()) {
571-
return self->pullAnimationMutations(timestamp);
572-
}
573-
return {};
574-
});
554+
if (!useSharedAnimatedBackend_) {
555+
// This method can be called from either the UI thread or JavaScript thread.
556+
// It ensures `startOnRenderCallback_` is called exactly once using atomic
557+
// operations. We use std::atomic_bool rather than std::mutex to avoid
558+
// potential deadlocks that could occur if we called external code while
559+
// holding a mutex.
560+
auto isRenderCallbackStarted = isRenderCallbackStarted_.exchange(true);
561+
if (isRenderCallbackStarted) {
562+
// onRender callback is already started.
563+
return;
564+
}
565+
if (startOnRenderCallback_) {
566+
startOnRenderCallback_([this]() { onRender(); }, isAsync);
575567
}
576-
577568
return;
578569
}
579570

580-
if (startOnRenderCallback_) {
581-
startOnRenderCallback_([this]() { onRender(); }, isAsync);
571+
{
572+
auto animationBackend = animationBackend_.lock();
573+
if (!animationBackend) {
574+
return;
575+
}
576+
// AnimationBackend::start registers the callback before returning the id.
577+
// Keep registration and id publication in one critical section; otherwise a
578+
// concurrent stop can observe no id and leave the registered callback
579+
// orphaned in the backend.
580+
std::lock_guard<std::mutex> lock(animationBackendCallbackMutex_);
581+
if (animationBackendCallbackId_.has_value()) {
582+
return;
583+
}
584+
auto weak = weak_from_this();
585+
auto callbackId = animationBackend->start(
586+
[weak](AnimationTimestamp timestamp) -> AnimationMutations {
587+
if (auto self = weak.lock()) {
588+
return self->pullAnimationMutations(timestamp);
589+
}
590+
return {};
591+
});
592+
animationBackendCallbackId_ = callbackId;
582593
}
583594
}
584595

585596
void NativeAnimatedNodesManager::stopRenderCallbackIfNeeded(
586597
bool isAsync) noexcept {
587-
// When multiple threads reach this point, only one thread should call
588-
// stopOnRenderCallback_. This synchronization is primarily needed during
589-
// destruction of NativeAnimatedNodesManager. In normal operation,
590-
// stopRenderCallbackIfNeeded is always called from the UI thread.
591-
auto isRenderCallbackStarted = isRenderCallbackStarted_.exchange(false);
592-
593-
if (useSharedAnimatedBackend_) {
598+
if (!useSharedAnimatedBackend_) {
599+
// When multiple threads reach this point, only one thread should call
600+
// stopOnRenderCallback_. This synchronization is primarily needed during
601+
// destruction of NativeAnimatedNodesManager. In normal operation,
602+
// stopRenderCallbackIfNeeded is always called from the UI thread.
603+
auto isRenderCallbackStarted = isRenderCallbackStarted_.exchange(false);
594604
if (isRenderCallbackStarted) {
595-
if (auto animationBackend = animationBackend_.lock()) {
596-
animationBackend->stop(animationBackendCallbackId_);
605+
if (stopOnRenderCallback_) {
606+
stopOnRenderCallback_(isAsync);
607+
608+
if (frameRateListenerCallback_) {
609+
frameRateListenerCallback_(false);
610+
}
597611
}
598612
}
599613
return;
600614
}
601615

602-
if (isRenderCallbackStarted) {
603-
if (stopOnRenderCallback_) {
604-
stopOnRenderCallback_(isAsync);
605-
606-
if (frameRateListenerCallback_) {
607-
frameRateListenerCallback_(false);
616+
{
617+
auto animationBackend = animationBackend_.lock();
618+
CallbackId callbackId;
619+
{
620+
std::lock_guard<std::mutex> lock(animationBackendCallbackMutex_);
621+
if (!animationBackendCallbackId_.has_value()) {
622+
return;
608623
}
624+
// Clear the published id while holding the same mutex that protects
625+
// start registration. This makes a concurrent start either see an active
626+
// callback or wait until this stop owns the callback id.
627+
callbackId = *animationBackendCallbackId_;
628+
animationBackendCallbackId_.reset();
629+
}
630+
if (animationBackend) {
631+
animationBackend->stop(callbackId);
609632
}
610633
}
611634
}

packages/react-native/ReactCommon/react/renderer/animated/NativeAnimatedNodesManager.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,10 @@ class NativeAnimatedNodesManager : public std::enable_shared_from_this<NativeAni
298298
bool warnedAboutGraphTraversal_ = false;
299299
#endif
300300

301-
CallbackId animationBackendCallbackId_{0};
301+
// Protects the register/publish and exchange/stop lifecycle for the shared
302+
// AnimationBackend callback.
303+
std::mutex animationBackendCallbackMutex_;
304+
std::optional<CallbackId> animationBackendCallbackId_;
302305

303306
friend class ColorAnimatedNode;
304307
friend class AnimationDriver;

0 commit comments

Comments
 (0)