diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp index f7393dbdd520..06f60bbcaf74 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp @@ -133,4 +133,8 @@ void RuntimeScheduler::setIntersectionObserverDelegate( intersectionObserverDelegate); } +void RuntimeScheduler::clear() noexcept { + return runtimeSchedulerImpl_->clear(); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h index 813537ec7aec..d81387d2d36a 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h @@ -64,6 +64,7 @@ class RuntimeSchedulerBase { virtual void setIntersectionObserverDelegate( RuntimeSchedulerIntersectionObserverDelegate* intersectionObserverDelegate) = 0; + virtual void clear() noexcept = 0; }; // This is a proxy for RuntimeScheduler implementation, which will be selected @@ -180,6 +181,8 @@ class RuntimeScheduler final : RuntimeSchedulerBase { RuntimeSchedulerIntersectionObserverDelegate* intersectionObserverDelegate) override; + void clear() noexcept override; + private: // Actual implementation, stored as a unique pointer to simplify memory // management. diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp index bd25d702aa73..5a0469ee2d11 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp @@ -246,6 +246,12 @@ void RuntimeScheduler_Legacy::setIntersectionObserverDelegate( // No-op in the legacy scheduler } +void RuntimeScheduler_Legacy::clear() noexcept { + // No-op: the legacy scheduler runs rendering updates synchronously in + // `scheduleRenderingUpdate`, so there is no queue to drain. See the header + // for details. +} + #pragma mark - Private void RuntimeScheduler_Legacy::scheduleWorkLoopIfNecessary() { diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h index 397786bc1fef..f36ef6488f9c 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h @@ -138,6 +138,8 @@ class RuntimeScheduler_Legacy final : public RuntimeSchedulerBase { RuntimeSchedulerIntersectionObserverDelegate* intersectionObserverDelegate) override; + void clear() noexcept override; + private: std::priority_queue< std::shared_ptr, diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp index 989125ee0372..edb35147e5a7 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp @@ -7,6 +7,8 @@ #include "RuntimeScheduler_Modern.h" +#include + #include #include #include @@ -189,6 +191,7 @@ void RuntimeScheduler_Modern::scheduleRenderingUpdate( RuntimeSchedulerRenderingUpdate&& renderingUpdate) { TraceSection s("RuntimeScheduler::scheduleRenderingUpdate"); + std::lock_guard lock(renderingUpdatesMutex_); surfaceIdsWithPendingRenderingUpdates_.insert(surfaceId); pendingRenderingUpdates_.push(renderingUpdate); } @@ -215,6 +218,23 @@ void RuntimeScheduler_Modern::setIntersectionObserverDelegate( intersectionObserverDelegate_ = intersectionObserverDelegate; } +void RuntimeScheduler_Modern::clear() noexcept { + TraceSection s("RuntimeScheduler::clear"); + + // Drop any pending rendering updates. The callbacks captured here may + // reference state owned by the caller (e.g. the `Scheduler`'s delegate); + // dropping them here guarantees they cannot be invoked on the JS thread + // after that state is destroyed. + std::lock_guard lock(renderingUpdatesMutex_); + auto droppedUpdates = pendingRenderingUpdates_.size(); + auto droppedSurfaces = surfaceIdsWithPendingRenderingUpdates_.size(); + pendingRenderingUpdates_ = {}; + surfaceIdsWithPendingRenderingUpdates_.clear(); + LOG(WARNING) << "RuntimeScheduler_Modern::clear() dropped " + << droppedUpdates << " pending rendering update(s) across " + << droppedSurfaces << " surface(s)."; +} + #pragma mark - Private void RuntimeScheduler_Modern::scheduleTask(std::shared_ptr task) { @@ -332,6 +352,13 @@ void RuntimeScheduler_Modern::runEventLoopTick( void RuntimeScheduler_Modern::updateRendering() { TraceSection s("RuntimeScheduler::updateRendering"); + // Hold the lock across the entire step so that `clear()` (called from + // another thread during `Scheduler` destruction) blocks until we are done + // running the callbacks. Otherwise `clear()` could return while we are + // still about to invoke lambdas that capture raw pointers into the + // now-destroyed `Scheduler`'s delegate. + std::lock_guard lock(renderingUpdatesMutex_); + // This is the integration of the Event Timing API in the Event Loop. // See https://w3c.github.io/event-timing/#sec-modifications-HTML const auto eventTimingDelegate = eventTimingDelegate_.load(); diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h index 83d96517fd3e..2d0a85c71fa4 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -155,6 +156,8 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase { RuntimeSchedulerIntersectionObserverDelegate* intersectionObserverDelegate) override; + void clear() noexcept override; + private: std::atomic syncTaskRequests_{0}; @@ -220,6 +223,14 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase { */ bool isEventLoopScheduled_{false}; + /** + * Protects `pendingRenderingUpdates_` and + * `surfaceIdsWithPendingRenderingUpdates_` so that the queue can be safely + * cleared from another thread (e.g. when the owning `Scheduler` is + * destroyed) without racing with the JS thread that normally pushes and + * drains it. + */ + std::mutex renderingUpdatesMutex_; std::queue pendingRenderingUpdates_; std::unordered_set surfaceIdsWithPendingRenderingUpdates_; diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp index bdcbdd60c474..3e8b94b0bba1 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -165,6 +165,23 @@ Scheduler::~Scheduler() { uiManager_->setDelegate(nullptr); uiManager_->setAnimationDelegate(nullptr); + // After detaching from UIManager, no more calls can be made into this + // Scheduler, so nothing new will be pushed into the RuntimeScheduler's + // rendering-update queue. Drop whatever is still queued: those lambdas + // capture raw pointers to this Scheduler's delegate (via + // `uiManagerDidDispatchCommand` / `uiManagerDidFinishTransaction`), and + // would otherwise fire on the JS thread after the delegate is destroyed. + // `clear()` blocks until any in-flight `updateRendering` finishes, so it + // is safe to destroy the delegate after this point. +#if __APPLE__ + if (runtimeScheduler) { + LOG(WARNING) + << "Scheduler::~Scheduler() clearing RuntimeScheduler rendering-update queue (address: " + << this << ")."; + runtimeScheduler->clear(); + } +#endif + // Then, let's verify that the requirement was satisfied. auto surfaceIds = std::vector{}; uiManager_->getShadowTreeRegistry().enumerate( diff --git a/private/cxx-public-api/ReactNativeCPP.api b/private/cxx-public-api/ReactNativeCPP.api index 8c2f51b8e1b5..4771e880eb56 100644 --- a/private/cxx-public-api/ReactNativeCPP.api +++ b/private/cxx-public-api/ReactNativeCPP.api @@ -35928,6 +35928,7 @@ class RuntimeSchedulerBase { virtual void setIntersectionObserverDelegate( RuntimeSchedulerIntersectionObserverDelegate* intersectionObserverDelegate) = 0; + virtual void clear() noexcept = 0; }; class RuntimeScheduler final : RuntimeSchedulerBase { public: @@ -35972,6 +35973,7 @@ class RuntimeScheduler final : RuntimeSchedulerBase { void setIntersectionObserverDelegate( RuntimeSchedulerIntersectionObserverDelegate* intersectionObserverDelegate) override; + void clear() noexcept override; }; } // namespace facebook::react @@ -36072,6 +36074,7 @@ class RuntimeScheduler_Legacy final : public RuntimeSchedulerBase { void setIntersectionObserverDelegate( RuntimeSchedulerIntersectionObserverDelegate* intersectionObserverDelegate) override; + void clear() noexcept override; }; std::atomic runtimeAccessRequests_{0}; std::atomic_bool isSynchronous_{false}; @@ -36136,6 +36139,7 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase { void setIntersectionObserverDelegate( RuntimeSchedulerIntersectionObserverDelegate* intersectionObserverDelegate) override; + void clear() noexcept override; }; std::priority_queue< std::shared_ptr, @@ -36172,6 +36176,7 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase { HighResTimeStamp endTime); std::function now_; bool isEventLoopScheduled_{false}; + std::mutex renderingUpdatesMutex_; std::queue pendingRenderingUpdates_; std::unordered_set surfaceIdsWithPendingRenderingUpdates_; std::atomic