diff --git a/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpp b/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpp index 531f0dc7b4eeb9b29cb2255d8444da02a74c35b7..534f419fce55c39a09a7eebfb7ab3c53f8a16637 100644 --- a/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpp +++ b/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpp @@ -1,8 +1,10 @@ #include #include +#include #include #include +#include namespace reanimated { @@ -25,25 +27,59 @@ void AnimatedPropsRegistry::update(jsi::Runtime &rt, const jsi::Value &operation addUpdatesToBatch(shadowNode, jsi::dynamicFromValue(rt, updates)); if constexpr (StaticFeatureFlags::getFlag("FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS")) { - timestampMap_[shadowNode->getTag()] = timestamp; + const auto tag = shadowNode->getTag(); + timestampMap_[tag] = timestamp; + // If JS already has a `settledProps` snapshot for this tag, it is now + // stale — schedule a refresh on the next `collectSettledUpdates`. + if (syncedTags_.erase(tag) > 0) { + invalidatedTags_.insert(tag); + } } } } -jsi::Value AnimatedPropsRegistry::getUpdatesOlderThanTimestamp( - jsi::Runtime &rt, - const double timestamp, - const double cleanupTimestamp) { +jsi::Value AnimatedPropsRegistry::collectSettledUpdates(jsi::Runtime &rt, const double settledTimestamp) { std::lock_guard lock{mutex_}; - removeUpdatesOlderThanTimestamp(cleanupTimestamp); std::vector>> updates; - for (const auto &[viewTag, pair] : updatesRegistry_) { - auto it = timestampMap_.find(viewTag); - if (it != timestampMap_.end() && it->second < timestamp) { - updates.emplace_back(viewTag, std::cref(pair.second)); + for (auto it = updatesRegistry_.begin(); it != updatesRegistry_.end();) { + const auto viewTag = it->first; + + if (syncedTags_.contains(viewTag)) { + // React already has the latest value for this tag (synced on a previous + // call, so the `settledProps` state is committed by now) — the registry + // entry is redundant. `syncedTags_` is intentionally retained to detect + // re-animation staleness. Note that `syncedTags_` and `invalidatedTags_` + // are disjoint — `update()` moves tags from the former to the latter. + timestampMap_.erase(viewTag); + it = updatesRegistry_.erase(it); + continue; + } + + const auto timestampIt = timestampMap_.find(viewTag); + if (timestampIt == timestampMap_.end()) { + ++it; + continue; + } + const bool isSettled = timestampIt->second < settledTimestamp; + const auto invalidatedIt = invalidatedTags_.find(viewTag); + const bool isInvalidated = invalidatedIt != invalidatedTags_.end(); + if (isSettled || isInvalidated) { + updates.emplace_back(viewTag, std::cref(it->second.second)); + if (isSettled) { + // Only settled-path tags are tracked as "synced" so that an ongoing + // animation doesn't re-trigger an invalidation/sync on every GC tick. + syncedTags_.insert(viewTag); + } + if (isInvalidated) { + // Only erase serviced invalidations; if a tag was invalidated but the + // matching update batch hasn't been flushed into updatesRegistry_ yet, + // we leave the entry so the next sync picks it up. + invalidatedTags_.erase(invalidatedIt); + } } + ++it; } const jsi::Array array(rt, updates.size()); @@ -58,22 +94,11 @@ jsi::Value AnimatedPropsRegistry::getUpdatesOlderThanTimestamp( return jsi::Value(rt, array); } -void AnimatedPropsRegistry::removeUpdatesOlderThanTimestamp(const double timestamp) { - for (auto it = timestampMap_.begin(); it != timestampMap_.end();) { - const auto viewTag = it->first; - const auto viewTimestamp = it->second; - if (viewTimestamp < timestamp) { - it = timestampMap_.erase(it); - updatesRegistry_.erase(viewTag); - } else { - it++; - } - } -} - void AnimatedPropsRegistry::removeTag(const Tag tag) { updatesRegistry_.erase(tag); timestampMap_.erase(tag); + syncedTags_.erase(tag); + invalidatedTags_.erase(tag); } } // namespace reanimated diff --git a/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.h b/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.h index 2c6c0e13604c9421e147d7eea7f4a4752288011c..8cd67f118501c2786b94d76541aea29a14ba8c16 100644 --- a/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.h +++ b/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.h @@ -4,10 +4,8 @@ #include -#include -#include #include -#include +#include namespace reanimated { @@ -15,13 +13,22 @@ class AnimatedPropsRegistry : public UpdatesRegistry { public: void update(jsi::Runtime &rt, const jsi::Value &operations, double timestamp); - /// Also removes updates older than `cleanupTimestamp` from the registry. - jsi::Value getUpdatesOlderThanTimestamp(jsi::Runtime &rt, double timestamp, double cleanupTimestamp); + /// Returns updates that settled (received no update since `settledTimestamp`) + /// or whose synced `settledProps` snapshot was invalidated by a fresh update. + /// Also evicts entries that have already been synced to React — by the time + /// of the next call, the corresponding `settledProps` state is guaranteed to + /// be committed, so the registry entries are redundant. + jsi::Value collectSettledUpdates(jsi::Runtime &rt, double settledTimestamp); private: std::unordered_map timestampMap_; // viewTag -> timestamp, protected by `mutex_` + // Tags whose latest values have already been pushed to React `settledProps`. + // Intentionally retained after eviction to detect re-animation staleness. + std::unordered_set syncedTags_; + // Tags that were synced to React but received a fresh worklet update since; + // their `settledProps` are stale and need to be refreshed on the next sync. + std::unordered_set invalidatedTags_; - void removeUpdatesOlderThanTimestamp(double timestamp); void removeTag(Tag tag) override; }; diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h index 096218ab9659955cd6272c97181bce3c893ed591..1a8e25fc8295b3ac943130709bf063ea41a50585 100644 --- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h +++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h @@ -57,11 +57,11 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele const SharedComponentDescriptorRegistry &componentDescriptorRegistry, const std::shared_ptr &contextContainer, jsi::Runtime &uiRuntime, - const std::shared_ptr &uiScheduler + const std::shared_ptr &uiScheduler, + const std::shared_ptr &uiManager #ifdef ANDROID , const PreserveMountedTagsFunction &filterUnmountedTagsFunction, - const std::shared_ptr &uiManager, const std::shared_ptr &jsInvoker #endif ) @@ -69,11 +69,11 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele contextContainer_(contextContainer), componentDescriptorRegistry_(componentDescriptorRegistry), uiRuntime_(uiRuntime), - uiScheduler_(uiScheduler) + uiScheduler_(uiScheduler), + uiManager_(uiManager) #ifdef ANDROID , preserveMountedTags_(filterUnmountedTagsFunction), - uiManager_(uiManager), jsInvoker_(jsInvoker) #endif { @@ -93,10 +93,10 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele SharedComponentDescriptorRegistry componentDescriptorRegistry_; jsi::Runtime &uiRuntime_; const std::shared_ptr uiScheduler_; + std::shared_ptr uiManager_; PreserveMountedTagsFunction preserveMountedTags_; #ifdef ANDROID - std::shared_ptr uiManager_; std::shared_ptr jsInvoker_; void restoreOpacityInCaseOfFlakyEnteringAnimation(SurfaceId surfaceId) const; diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h index eca44e4cf651d16e9741806004ce9119c85d07d6..e39c79a5d7b52659106ed6fe6fbcbbc048bf4787 100644 --- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h +++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h @@ -66,11 +66,11 @@ struct LayoutAnimationsProxy_Experimental : public LayoutAnimationsProxyCommon, const SharedComponentDescriptorRegistry &componentDescriptorRegistry, const std::shared_ptr &contextContainer, jsi::Runtime &uiRuntime, - const std::shared_ptr &uiScheduler + const std::shared_ptr &uiScheduler, + const std::shared_ptr &uiManager #ifdef ANDROID , const PreserveMountedTagsFunction &filterUnmountedTagsFunction, - const std::shared_ptr &uiManager, const std::shared_ptr &jsInvoker #endif ) @@ -79,11 +79,11 @@ struct LayoutAnimationsProxy_Experimental : public LayoutAnimationsProxyCommon, componentDescriptorRegistry, contextContainer, uiRuntime, - uiScheduler + uiScheduler, + uiManager #ifdef ANDROID , filterUnmountedTagsFunction, - uiManager, jsInvoker #endif ), diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp index 83ef7430b923b6b3b99368ee8072168769110cd0..2affd12822ab19bdc90963d3ce8ca1e6bb0d43b7 100644 --- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp +++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -53,14 +54,37 @@ std::optional LayoutAnimationsProxy_Legacy::pullTransaction parseRemoveMutations(movedViews, mutations, roots); - auto shouldAnimate = !surfacesToRemove_.contains(surfaceId); - surfacesToRemove_.erase(surfaceId); + // Consume the teardown mark only on the transaction that actually clears + // the root — pulls emitted for animation frames must not eat it early. + auto shouldAnimate = true; + const auto removesRootChildren = std::ranges::any_of(mutations, [surfaceId](const auto &mutation) { + return mutation.type == ShadowViewMutation::Remove && mutation.parentTag == surfaceId; + }); + if (removesRootChildren) { + shouldAnimate = surfacesToRemove_.erase(surfaceId) == 0; + } handleRemovals(filteredMutations, roots, deadNodes, shouldAnimate); handleUpdatesAndEnterings(filteredMutations, movedViews, mutations, propsParserContext, surfaceId); addOngoingAnimations(surfaceId, filteredMutations); + // The LayoutAnimationDriver can emit a final keyframe update in the same + // transaction as the deferred Remove/Delete it withheld for a delete + // animation. We emit removals before updates, so such an update would + // otherwise reach the mounting layer after its view was deleted. + std::unordered_set deletedTags; + for (const auto &mutation : filteredMutations) { + if (mutation.type == ShadowViewMutation::Delete) { + deletedTags.insert(mutation.oldChildShadowView.tag); + } + } + if (!deletedTags.empty()) { + std::erase_if(filteredMutations, [&deletedTags](const auto &mutation) { + return mutation.type == ShadowViewMutation::Update && deletedTags.contains(mutation.newChildShadowView.tag); + }); + } + return MountingTransaction{surfaceId, transactionNumber, std::move(filteredMutations), telemetry}; } @@ -947,23 +971,22 @@ inline bool MutationNode::isMutationNode() { return true; } -// UIManagerAnimationDelegate - -void LayoutAnimationsProxy_Legacy::uiManagerDidConfigureNextLayoutAnimation( - jsi::Runtime &runtime, - const RawValue &config, - const jsi::Value &successCallbackValue, - const jsi::Value &failureCallbackValue) const {} +// UIManagerCommitHook -void LayoutAnimationsProxy_Legacy::setComponentDescriptorRegistry( - const SharedComponentDescriptorRegistry &componentDescriptorRegistry) {} - -bool LayoutAnimationsProxy_Legacy::shouldAnimateFrame() const { - return false; -} - -void LayoutAnimationsProxy_Legacy::stopSurface(SurfaceId surfaceId) { - surfacesToRemove_.insert(surfaceId); +// Surface teardown commits an empty root (SurfaceHandler::stop) before the +// teardown transaction is pulled — mark it so pullTransaction skips exit +// animations. Reading the ShadowTreeRegistry here instead would deadlock (#8579). +RootShadowNode::Unshared LayoutAnimationsProxy_Legacy::shadowTreeWillCommit( + const ShadowTree &shadowTree, + const RootShadowNode::Shared & /*oldRootShadowNode*/, + const RootShadowNode::Unshared &newRootShadowNode) noexcept { + auto lock = std::unique_lock(mutex); + if (newRootShadowNode->getChildren().empty()) { + surfacesToRemove_.insert(shadowTree.getSurfaceId()); + } else { + surfacesToRemove_.erase(shadowTree.getSurfaceId()); + } + return newRootShadowNode; } } // namespace reanimated diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h index e9a5e9959e89ec33cee179ddb907c17f6dfbd3de..a2c89041518cd71e8ba5ac62ef89c0022d197c9b 100644 --- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h +++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h @@ -3,8 +3,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -102,7 +102,7 @@ struct SurfaceContext { }; struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon, - public UIManagerAnimationDelegate, + public UIManagerCommitHook, public std::enable_shared_from_this { mutable std::unordered_map> nodeForTag_; mutable std::recursive_mutex mutex; @@ -116,11 +116,11 @@ struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon, const SharedComponentDescriptorRegistry &componentDescriptorRegistry, const std::shared_ptr &contextContainer, jsi::Runtime &uiRuntime, - const std::shared_ptr &uiScheduler + const std::shared_ptr &uiScheduler, + const std::shared_ptr &uiManager #ifdef ANDROID , const PreserveMountedTagsFunction &filterUnmountedTagsFunction, - const std::shared_ptr &uiManager, const std::shared_ptr &jsInvoker #endif ) @@ -129,14 +129,19 @@ struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon, componentDescriptorRegistry, contextContainer, uiRuntime, - uiScheduler + uiScheduler, + uiManager #ifdef ANDROID , filterUnmountedTagsFunction, - uiManager, jsInvoker #endif ) { + uiManager->registerCommitHook(*this); + } + + ~LayoutAnimationsProxy_Legacy() override { + uiManager_->unregisterCommitHook(*this); } void startEnteringAnimation(const int tag, ShadowViewMutation &mutation) const; @@ -202,19 +207,15 @@ struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon, const TransactionTelemetry &telemetry, ShadowViewMutationList mutations) const override; - // UIManagerAnimationDelegate - - void uiManagerDidConfigureNextLayoutAnimation( - jsi::Runtime &runtime, - const RawValue &config, - const jsi::Value &successCallbackValue, - const jsi::Value &failureCallbackValue) const override; - - void setComponentDescriptorRegistry(const SharedComponentDescriptorRegistry &componentDescriptorRegistry) override; + // UIManagerCommitHook - bool shouldAnimateFrame() const override; + void commitHookWasRegistered(const UIManager &uiManager) noexcept override {} + void commitHookWasUnregistered(const UIManager &uiManager) noexcept override {} - void stopSurface(SurfaceId surfaceId) override; + RootShadowNode::Unshared shadowTreeWillCommit( + const ShadowTree &shadowTree, + const RootShadowNode::Shared &oldRootShadowNode, + const RootShadowNode::Unshared &newRootShadowNode) noexcept override; }; } // namespace reanimated diff --git a/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp b/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp index 9ade22bf773005613048a00c47b35767628e86c6..f3415e824da1a8da5c83762415ca54646bd6429f 100644 --- a/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp +++ b/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp @@ -524,15 +524,13 @@ jsi::Value ReanimatedModuleProxy::getSettledUpdates(jsi::Runtime &rt) { StaticFeatureFlags::getFlag("FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS") && "getSettledUpdates requires FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS static feature flag to be enabled"); + constexpr double SETTLED_ANIMATION_THRESHOLD_MS = 1000; + // TODO(future): use unified timestamp const auto currentTimestamp = getAnimationTimestamp_(); - // TODO: fix bug when threshold difference is smaller than 1 second // TODO(future): flush updates from CSS animations and CSS transitions registries - // TODO(future): find a better way to obtain timestamp for removing updates - // TODO(future): move removing old updates to separate method - return animatedPropsRegistry_->getUpdatesOlderThanTimestamp( - rt, currentTimestamp - 1000 /* 1 second */, currentTimestamp - 2000 /* 2 seconds */); + return animatedPropsRegistry_->collectSettledUpdates(rt, currentTimestamp - SETTLED_ANIMATION_THRESHOLD_MS); } bool ReanimatedModuleProxy::handleEvent( @@ -1306,11 +1304,11 @@ void ReanimatedModuleProxy::initializeLayoutAnimationsProxy() { componentDescriptorRegistry, scheduler->getContextContainer(), getJSIRuntimeFromWorkletRuntime(uiRuntime_), - uiScheduler_ + uiScheduler_, + uiManager_ #ifdef ANDROID , filterUnmountedTagsFunction_, - uiManager_, jsInvoker_ #endif ); @@ -1319,22 +1317,19 @@ void ReanimatedModuleProxy::initializeLayoutAnimationsProxy() { #endif layoutAnimationsProxy_ = std::move(layoutAnimationsProxyExperimental); } else { - auto layoutAnimationsProxyLegacy = std::make_shared( + layoutAnimationsProxy_ = std::make_shared( layoutAnimationsManager_, componentDescriptorRegistry, scheduler->getContextContainer(), getJSIRuntimeFromWorkletRuntime(uiRuntime_), - uiScheduler_ + uiScheduler_, + uiManager_ #ifdef ANDROID , filterUnmountedTagsFunction_, - uiManager_, jsInvoker_ #endif ); - // TODO (future): support in experimental - uiManager_->setAnimationDelegate(layoutAnimationsProxyLegacy.get()); - layoutAnimationsProxy_ = std::move(layoutAnimationsProxyLegacy); } } } diff --git a/src/PropsRegistryGarbageCollector.ts b/src/PropsRegistryGarbageCollector.ts index f917ce5a8586c02855f1d8d9ae73154592d22510..32148fbac8a9224ffec6edc784b48938da9585fb 100644 --- a/src/PropsRegistryGarbageCollector.ts +++ b/src/PropsRegistryGarbageCollector.ts @@ -11,7 +11,6 @@ import { ReanimatedModule } from './ReanimatedModule'; const FLUSH_INTERVAL_MS = 500; export const PropsRegistryGarbageCollector = { - viewsCount: 0, viewsMap: new Map(), intervalId: null as NodeJS.Timeout | null, @@ -25,16 +24,14 @@ export const PropsRegistryGarbageCollector = { return; } this.viewsMap.set(viewTag, component); - this.viewsCount++; - if (this.viewsCount === 1) { + if (this.viewsMap.size === 1) { this.registerInterval(); } }, unregisterView(viewTag: number) { - this.viewsMap.delete(viewTag); - this.viewsCount--; - if (this.viewsCount === 0) { + const deleted = this.viewsMap.delete(viewTag); + if (deleted && this.viewsMap.size === 0) { this.unregisterInterval(); } },