From c2acb707cc31999c509c10cec97e538b710e9412 Mon Sep 17 00:00:00 2001 From: Patrick Kabwe Date: Wed, 26 Aug 2026 14:38:57 +0200 Subject: [PATCH] fix: prune expired JSICache weak references --- .../NitroModules.podspec | 1 + .../cpp/jsi/JSICache.cpp | 24 ++---- .../cpp/jsi/JSICache.hpp | 26 +++--- .../cpp/utils/WeakReferenceCache.hpp | 80 +++++++++++++++++++ 4 files changed, 101 insertions(+), 30 deletions(-) create mode 100644 packages/react-native-nitro-modules/cpp/utils/WeakReferenceCache.hpp diff --git a/packages/react-native-nitro-modules/NitroModules.podspec b/packages/react-native-nitro-modules/NitroModules.podspec index 9163edcc15..a0d3e1bf82 100644 --- a/packages/react-native-nitro-modules/NitroModules.podspec +++ b/packages/react-native-nitro-modules/NitroModules.podspec @@ -50,6 +50,7 @@ Pod::Spec.new do |s| "cpp/utils/NitroHash.hpp", "cpp/utils/NitroDefines.hpp", "cpp/utils/PropNameIDCache.hpp", + "cpp/utils/WeakReferenceCache.hpp", "cpp/views/CachedProp.hpp", "cpp/views/RawPropsCompat.hpp", "cpp/views/ReactProp.hpp", diff --git a/packages/react-native-nitro-modules/cpp/jsi/JSICache.cpp b/packages/react-native-nitro-modules/cpp/jsi/JSICache.cpp index 45739e3e8f..a1c8154cb8 100644 --- a/packages/react-native-nitro-modules/cpp/jsi/JSICache.cpp +++ b/packages/react-native-nitro-modules/cpp/jsi/JSICache.cpp @@ -12,27 +12,17 @@ namespace margelo::nitro { -template -inline void destroyReferences(const std::vector>& references) { - for (auto& func : references) { - BorrowingReference reference = func.lock(); - if (reference) { - // Destroy all functions that we might still have in cache, some callbacks and Promises may now become invalid. - reference.destroy(); - } - } -} - JSICache::~JSICache() { Logger::log(LogLevel::Info, TAG, "Destroying JSICache..."); std::unique_lock lock(_mutex); - destroyReferences(_valueCache); - destroyReferences(_objectCache); - destroyReferences(_functionCache); - destroyReferences(_weakObjectCache); - destroyReferences(_propNameIDCache); - destroyReferences(_arrayBufferCache); + // Some callbacks and Promises may now become invalid. + _valueCache.destroyAll(); + _objectCache.destroyAll(); + _functionCache.destroyAll(); + _weakObjectCache.destroyAll(); + _propNameIDCache.destroyAll(); + _arrayBufferCache.destroyAll(); } JSICacheReference JSICache::getOrCreateCache(jsi::Runtime& runtime) { diff --git a/packages/react-native-nitro-modules/cpp/jsi/JSICache.hpp b/packages/react-native-nitro-modules/cpp/jsi/JSICache.hpp index b44abe5b04..5e93bb101e 100644 --- a/packages/react-native-nitro-modules/cpp/jsi/JSICache.hpp +++ b/packages/react-native-nitro-modules/cpp/jsi/JSICache.hpp @@ -10,12 +10,12 @@ #include "BorrowingReference.hpp" #include "NitroLogger.hpp" #include "WeakReference.hpp" +#include "WeakReferenceCache.hpp" #include #include #include #include #include -#include namespace margelo::nitro { @@ -60,12 +60,12 @@ class JSICache final : public jsi::NativeState { private: std::mutex _mutex; - std::vector> _valueCache; - std::vector> _objectCache; - std::vector> _functionCache; - std::vector> _weakObjectCache; - std::vector> _propNameIDCache; - std::vector> _arrayBufferCache; + WeakReferenceCache _valueCache; + WeakReferenceCache _objectCache; + WeakReferenceCache _functionCache; + WeakReferenceCache _weakObjectCache; + WeakReferenceCache _propNameIDCache; + WeakReferenceCache _arrayBufferCache; private: static inline std::unordered_map> _globalCache; @@ -87,32 +87,32 @@ class JSICacheReference final { public: BorrowingReference makeShared(jsi::Value&& value) { BorrowingReference owning(new jsi::Value(std::move(value))); - _strongCache->_valueCache.push_back(owning.weak()); + _strongCache->_valueCache.push(owning.weak()); return owning; } BorrowingReference makeShared(jsi::Object&& value) { BorrowingReference owning(new jsi::Object(std::move(value))); - _strongCache->_objectCache.push_back(owning.weak()); + _strongCache->_objectCache.push(owning.weak()); return owning; } BorrowingReference makeShared(jsi::Function&& value) { BorrowingReference owning(new jsi::Function(std::move(value))); - _strongCache->_functionCache.push_back(owning.weak()); + _strongCache->_functionCache.push(owning.weak()); return owning; } BorrowingReference makeShared(jsi::WeakObject&& value) { BorrowingReference owning(new jsi::WeakObject(std::move(value))); - _strongCache->_weakObjectCache.push_back(owning.weak()); + _strongCache->_weakObjectCache.push(owning.weak()); return owning; } BorrowingReference makeShared(jsi::PropNameID&& value) { BorrowingReference owning(new jsi::PropNameID(std::move(value))); - _strongCache->_propNameIDCache.push_back(owning.weak()); + _strongCache->_propNameIDCache.push(owning.weak()); return owning; } BorrowingReference makeShared(jsi::ArrayBuffer&& value) { BorrowingReference owning(new jsi::ArrayBuffer(std::move(value))); - _strongCache->_arrayBufferCache.push_back(owning.weak()); + _strongCache->_arrayBufferCache.push(owning.weak()); return owning; } diff --git a/packages/react-native-nitro-modules/cpp/utils/WeakReferenceCache.hpp b/packages/react-native-nitro-modules/cpp/utils/WeakReferenceCache.hpp new file mode 100644 index 0000000000..03befb9321 --- /dev/null +++ b/packages/react-native-nitro-modules/cpp/utils/WeakReferenceCache.hpp @@ -0,0 +1,80 @@ +// +// WeakReferenceCache.hpp +// react-native-nitro +// +// Created by Patrick Kabwe on 25.08.26. +// + +#pragma once + +#include "BorrowingReference.hpp" +#include "WeakReference.hpp" +#include +#include +#include +#include + +namespace margelo::nitro { + +/** + * Holds `WeakReference`s to values that have to be destroyed once their + * `jsi::Runtime` goes away. + * + * A `WeakReference` keeps its `ReferenceState` alive after its value has been + * destroyed. The cache periodically removes expired references to release that + * state before Runtime teardown. + * + * Compaction runs at geometrically increasing size thresholds instead of on + * every insert, which keeps `push(...)` amortized O(1). Expired entries can + * remain until a later insertion reaches the next threshold. + */ +template +class WeakReferenceCache final { +public: + WeakReferenceCache() = default; + + WeakReferenceCache(const WeakReferenceCache&) = delete; + WeakReferenceCache(WeakReferenceCache&&) = delete; + + void push(WeakReference&& reference) { + _references.push_back(std::move(reference)); + if (_references.size() >= _nextPruneThreshold) [[unlikely]] { + pruneExpired(); + } + } + + void destroyAll() { + for (const WeakReference& reference : _references) { + BorrowingReference value = reference.lock(); + if (value != nullptr) { + value.destroy(); + } + } + } + + [[nodiscard]] + std::size_t size() const noexcept { + return _references.size(); + } + +private: + void pruneExpired() { + const auto isExpired = [](const WeakReference& reference) { + return reference.lock() == nullptr; + }; + _references.erase(std::remove_if(_references.begin(), _references.end(), isExpired), _references.end()); + _nextPruneThreshold = std::max(MIN_PRUNE_BATCH_SIZE, _references.size() * 2); + } + +private: + // Minimum number of cached weak refs to accumulate before pruning expired + // entries. This keeps insertion amortized O(1) while bounding retained + // ReferenceState slack. + static constexpr std::size_t MIN_PRUNE_BATCH_SIZE = 64; + +private: + std::vector> _references; + std::size_t _nextPruneThreshold = MIN_PRUNE_BATCH_SIZE; +}; + +} // namespace margelo::nitro