Skip to content

Commit 7c86624

Browse files
authored
fix(Worklets): RemoteFunction memory cycle (#9673)
## Summary Fixes #9661 In #9272 which was supposed to fix memory issues with Remote Function, I accidentally introduced a memory-leak due to reference retain cycle. ### Before Basically, RemoteFunctionRegistry would hold a strong reference to the (remote) function, which in turn would hold a strong reference to the serialized remote function, which would hold the C++ value. This C++ value would erase the original function from the registry, but only in its destructor - which would never happen, because the registry itself would keep it alive forever. ```mermaid flowchart TB subgraph RN[RN Runtime] R[RemoteFunctionRegistry] F[remote function] C[serialized remote function] end subgraph CPP[C++ heap] S[C++ value] end subgraph WK[Worker Runtime] H[holderFunction] end R --> F F -.-> C C --> S H --> S S ==>|"registry.delete(id) — never invoked"| R linkStyle 0,2,3 stroke:#2f9e44,stroke-width:2px linkStyle 1 stroke:#868e96,stroke-width:2px linkStyle 4 stroke:#e03131,stroke-width:3px ``` ### After To fix this issue, I split the C++ Remote Function for RN callbacks into two subclasses - one would only be owned by the RN Runtime, and the other one from Worklet Runtimes. If all Worklet Runtime references would get cleaned up, then they would schedule cleanup on the RN Runtime C++ side, to erase the original function from the registry. Then, this would in turn release the cache value and the remaining C++ value. ```mermaid flowchart TB subgraph RN[RN Runtime] R[RemoteFunctionRegistry] F[remote function] C[serialized remote function] end subgraph CPP[C++ heap] O[RN-owned C++ value] P[Worklet-owned C++ value] end subgraph WK[Worker Runtime] H[holderFunction] end R --> F F -.-> C C --> O H --> P P --> O O -.-> P P ==>|"registry.delete(id)"| R linkStyle 0,2,3,4 stroke:#2f9e44,stroke-width:2px linkStyle 1,5 stroke:#868e96,stroke-width:2px linkStyle 6 stroke:#2f9e44,stroke-width:3px ``` ## Test Plan I used the following code and was tracking constructor and destructor invocations of `SerializableRemoteFunction` in debugger. <details><summary>Code</summary> <p> ```tsx import { useEffect } from 'react'; import { Button, StyleSheet, View } from 'react-native'; import { scheduleOnRN, scheduleOnUI, createWorkletRuntime, scheduleOnRuntime, createSerializable, } from 'react-native-worklets'; const workerRuntime = createWorkletRuntime(); export default function App() { useEffect(() => { scheduleOnUI(() => { function foo() { const _ = new Array(10000).fill(0); requestAnimationFrame(foo); } foo(); }); }); return ( <View style={styles.container}> <Button title="Init runtime" onPress={() => { scheduleOnUI(() => { // scheduleOnRuntime(workerRuntime, () => { 'worklet'; console.log('init'); }); }} /> <Button title="Press me first" onPress={() => { function logTest() { console.log('Button pressed!'); } // scheduleOnRuntime(workerRuntime, () => { scheduleOnUI(() => { 'worklet'; scheduleOnRN(logTest); }); }} /> <Button title="Trigger GC on RN" onPress={() => { console.log('Triggering GC on RN'); gc(); }} /> <Button title="Trigger GC on Worker" onPress={() => { // scheduleOnRuntime(workerRuntime, () => { scheduleOnUI(() => { 'worklet'; console.log('Triggering GC on Worker'); gc(); }); }} /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, }); ``` </p> </details>
1 parent 4eda300 commit 7c86624

8 files changed

Lines changed: 275 additions & 143 deletions

File tree

packages/react-native-worklets/Common/cpp/worklets/Compat/StableApi.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include <worklets/Compat/Holders.h>
33
#include <worklets/Compat/StableApi.h>
44
#include <worklets/SharedItems/Serializable.h>
5+
#include <worklets/SharedItems/SerializableRemoteFunction.h>
56
#include <worklets/SharedItems/Shareable.h>
67
#include <worklets/SharedItems/Synchronizable.h>
78
#include <worklets/Tools/JSISerializer.h>

packages/react-native-worklets/Common/cpp/worklets/NativeModules/JSIWorkletsModuleProxy.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <worklets/NativeModules/JSIWorkletsModuleProxy.h>
66
#include <worklets/SharedItems/Serializable.h>
77
#include <worklets/SharedItems/SerializableFactory.h>
8+
#include <worklets/SharedItems/SerializableRemoteFunction.h>
89
#include <worklets/SharedItems/Shareable.h>
910
#include <worklets/SharedItems/Synchronizable.h>
1011
#include <worklets/Tools/FeatureFlags.h>
@@ -348,11 +349,11 @@ jsi::Object JSIWorkletsModuleProxy::toOptimizedObject(jsi::Runtime &rt) const {
348349
}
349350
if (hostRuntimeId == RuntimeData::rnRuntimeId) {
350351
const int remoteId = static_cast<int>(at<1>(args).getNumber());
351-
auto ref = makeSerializableRemoteFunction(rt, name, remoteId, jsScheduler);
352+
auto ref = makeRNOriginSerializableRemoteFunction(rt, name, remoteId, jsScheduler);
352353
ref.asObject(rt).setProperty(rt, "__keepAlive", true);
353354
return ref;
354355
}
355-
return makeSerializableRemoteFunction(rt, name, std::move(fun), hostRuntimeId);
356+
return makeWorkletOriginSerializableRemoteFunction(rt, name, std::move(fun), hostRuntimeId);
356357
});
357358

358359
jsi_utils::addMethod<2>(

packages/react-native-worklets/Common/cpp/worklets/SharedItems/Serializable.cpp

Lines changed: 1 addition & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <worklets/WorkletRuntime/WorkletRuntime.h>
88

99
#include <memory>
10+
#include <stdexcept>
1011
#include <string>
1112
#include <utility>
1213

@@ -22,18 +23,6 @@ jsi::Function getValueUnpacker(jsi::Runtime &rt) {
2223
return valueUnpacker.asObject(rt).asFunction(rt);
2324
}
2425

25-
jsi::Function getRemoteFunctionUnpacker(jsi::Runtime &rt) {
26-
auto remoteFunctionUnpacker = rt.global().getProperty(rt, "__remoteFunctionUnpacker");
27-
react_native_assert(remoteFunctionUnpacker.isObject() && "remoteFunctionUnpacker not found");
28-
return remoteFunctionUnpacker.asObject(rt).asFunction(rt);
29-
}
30-
31-
jsi::Object getRemoteFunctionRegistry(jsi::Runtime &rt) {
32-
auto registry = rt.global().getProperty(rt, "__remoteFunctionRegistry");
33-
react_native_assert(registry.isObject() && "remoteFunctionRegistry not found");
34-
return registry.getObject(rt);
35-
}
36-
3726
} // namespace
3827

3928
jsi::Value makeSerializableClone(
@@ -302,61 +291,6 @@ jsi::Value SerializableImport::toJSValue(jsi::Runtime &rt) {
302291
return metroRequire.asObject(rt).asFunction(rt).call(rt, source_).asObject(rt).getProperty(rt, imported);
303292
}
304293

305-
SerializableRemoteFunction::~SerializableRemoteFunction() {
306-
if (isHostedOnRNRuntime()) {
307-
// TODO: consider batching
308-
const auto &data = std::get<RNRuntimeData>(runtimeData_);
309-
data.jsScheduler->scheduleOnJS([id = data.remoteId](jsi::Runtime &rt) {
310-
const auto registry = getRemoteFunctionRegistry(rt);
311-
registry.getPropertyAsFunction(rt, "delete").callWithThis(rt, registry, jsi::Value(id));
312-
});
313-
} else {
314-
auto &workletData = std::get<WorkletRuntimeData>(runtimeData_);
315-
cleanupRuntimeAware(hostRuntime_, workletData.function);
316-
}
317-
}
318-
319-
jsi::Value SerializableRemoteFunction::toJSValue(jsi::Runtime &rt) {
320-
if (&rt == hostRuntime_) {
321-
if (isHostedOnRNRuntime()) {
322-
const auto &rnData = std::get<RNRuntimeData>(runtimeData_);
323-
const auto registry = getRemoteFunctionRegistry(rt);
324-
return registry.getPropertyAsFunction(rt, "get").callWithThis(rt, registry, jsi::Value(rnData.remoteId));
325-
} else {
326-
const auto &workletData = std::get<WorkletRuntimeData>(runtimeData_);
327-
return jsi::Value(rt, *workletData.function);
328-
}
329-
} else {
330-
const auto name = name_.empty() ? jsi::Value::undefined() : jsi::String::createFromUtf8(rt, name_);
331-
auto holderFunction = getRemoteFunctionUnpacker(rt).call(rt, name).getObject(rt);
332-
holderFunction.setNativeState(rt, std::make_shared<SerializableJSRef>(shared_from_this()));
333-
return holderFunction;
334-
}
335-
}
336-
337-
// TODO: generalize it and merge with other scheduling methods
338-
void SerializableRemoteFunction::resolveOrRejectPromise(
339-
const std::shared_ptr<Serializable> &resolveValue,
340-
const std::shared_ptr<RuntimeManager> &runtimeManager) {
341-
if (isHostedOnRNRuntime()) {
342-
const auto &data = std::get<RNRuntimeData>(runtimeData_);
343-
data.jsScheduler->scheduleOnJS([resolver = shared_from_this(), resolveValue](jsi::Runtime &rt) {
344-
resolver->toJSValue(rt).getObject(rt).getFunction(rt).call(rt, resolveValue->toJSValue(rt));
345-
});
346-
} else {
347-
const auto workletRuntime = runtimeManager->getRuntime(hostRuntimeId_);
348-
// NOLINTNEXTLINE(readability/braces)
349-
if (!workletRuntime) [[unlikely]] {
350-
// Host runtime is dead, most likely we're the last owner of the Remote Function.
351-
// Do nothing.
352-
} else {
353-
workletRuntime->schedule([resolver = shared_from_this(), resolveValue](jsi::Runtime &rt) {
354-
resolver->toJSValue(rt).getObject(rt).getFunction(rt).call(rt, resolveValue->toJSValue(rt));
355-
});
356-
}
357-
}
358-
}
359-
360294
jsi::Value SerializableInitializer::toJSValue(jsi::Runtime &rt) {
361295
if (remoteValue_ == nullptr) {
362296
auto initObj = initializer_->toJSValue(rt);

packages/react-native-worklets/Common/cpp/worklets/SharedItems/Serializable.h

Lines changed: 1 addition & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <worklets/WorkletRuntime/RuntimeData.h>
88

99
#include <memory>
10+
#include <mutex>
1011
#include <optional>
1112
#include <string>
1213
#include <utility>
@@ -289,71 +290,6 @@ class SerializableImport : public Serializable {
289290
const std::string imported_;
290291
};
291292

292-
/** Forward declaration */
293-
class RuntimeManager;
294-
295-
class SerializableRemoteFunction : public Serializable,
296-
public std::enable_shared_from_this<SerializableRemoteFunction> {
297-
private:
298-
struct RNRuntimeData {
299-
const int remoteId;
300-
const std::shared_ptr<JSScheduler> jsScheduler;
301-
};
302-
303-
struct WorkletRuntimeData {
304-
std::unique_ptr<jsi::Value> function;
305-
};
306-
307-
jsi::Runtime *hostRuntime_;
308-
const RuntimeData::RuntimeId hostRuntimeId_;
309-
std::variant<RNRuntimeData, WorkletRuntimeData> runtimeData_;
310-
const std::string name_;
311-
312-
public:
313-
/** Creates RN Runtime Remote Function. */
314-
SerializableRemoteFunction(
315-
jsi::Runtime &rnRuntime,
316-
const std::string &name,
317-
const int remoteId,
318-
const std::shared_ptr<JSScheduler> &jsScheduler)
319-
: Serializable(ValueType::RemoteFunctionType),
320-
hostRuntime_(&rnRuntime),
321-
hostRuntimeId_(RuntimeData::rnRuntimeId),
322-
runtimeData_(RNRuntimeData{.remoteId = remoteId, .jsScheduler = jsScheduler}),
323-
name_(name) {}
324-
325-
/** Creates Worklet Runtime Remote Function. */
326-
SerializableRemoteFunction(
327-
jsi::Runtime &workletRuntime,
328-
const std::string &name,
329-
jsi::Function &&function,
330-
RuntimeData::RuntimeId hostRuntimeId)
331-
: Serializable(ValueType::RemoteFunctionType),
332-
hostRuntime_(&workletRuntime),
333-
hostRuntimeId_(hostRuntimeId),
334-
runtimeData_(WorkletRuntimeData{.function = std::make_unique<jsi::Value>(workletRuntime, std::move(function))}),
335-
name_(name) {}
336-
337-
~SerializableRemoteFunction() override;
338-
339-
SerializableRemoteFunction(const SerializableRemoteFunction &) = delete;
340-
SerializableRemoteFunction &operator=(const SerializableRemoteFunction &) = delete;
341-
342-
void resolveOrRejectPromise(
343-
const std::shared_ptr<Serializable> &resolveValue,
344-
const std::shared_ptr<RuntimeManager> &runtimeManager);
345-
346-
jsi::Value toJSValue(jsi::Runtime &rt) override;
347-
348-
[[nodiscard]] bool isHostedOnRNRuntime() const noexcept {
349-
return std::holds_alternative<RNRuntimeData>(runtimeData_);
350-
}
351-
352-
[[nodiscard]] RuntimeData::RuntimeId getHostRuntimeId() const {
353-
return hostRuntimeId_;
354-
}
355-
};
356-
357293
class SerializableInitializer : public Serializable {
358294
private:
359295
// We don't release the initializer since the handle can get

packages/react-native-worklets/Common/cpp/worklets/SharedItems/SerializableFactory.cpp

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include <jsi/jsi.h>
22
#include <worklets/SharedItems/SerializableFactory.h>
3+
#include <worklets/SharedItems/SerializableRemoteFunction.h>
34

45
#include <memory>
56
#include <utility>
@@ -60,22 +61,22 @@ jsi::Value makeSerializableHostFunction(
6061
return SerializableJSRef::newNativeStateObject(rt, serializable);
6162
}
6263

63-
jsi::Value makeSerializableRemoteFunction(
64+
jsi::Value makeRNOriginSerializableRemoteFunction(
6465
jsi::Runtime &rnRuntime,
6566
const std::string &name,
6667
const int remoteId,
6768
const std::shared_ptr<JSScheduler> &jsScheduler) {
68-
auto serializable = std::make_shared<SerializableRemoteFunction>(rnRuntime, name, remoteId, jsScheduler);
69+
auto serializable = std::make_shared<SerializableRemoteFunction::RNOrigin>(rnRuntime, name, remoteId, jsScheduler);
6970
return SerializableJSRef::newNativeStateObject(rnRuntime, serializable);
7071
}
7172

72-
jsi::Value makeSerializableRemoteFunction(
73+
jsi::Value makeWorkletOriginSerializableRemoteFunction(
7374
jsi::Runtime &workletRuntime,
7475
const std::string &name,
7576
jsi::Function &&function,
7677
RuntimeData::RuntimeId hostRuntimeId) {
77-
auto serializable =
78-
std::make_shared<SerializableRemoteFunction>(workletRuntime, name, std::move(function), hostRuntimeId);
78+
auto serializable = std::make_shared<SerializableRemoteFunction::WorkletOrigin>(
79+
workletRuntime, name, std::move(function), hostRuntimeId);
7980
return SerializableJSRef::newNativeStateObject(workletRuntime, serializable);
8081
}
8182

packages/react-native-worklets/Common/cpp/worklets/SharedItems/SerializableFactory.h

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,13 @@ jsi::Value makeSerializableHostFunction(
6363
const std::string &name,
6464
unsigned int paramCount);
6565

66-
/** Creates RN Runtime Remote Function. */
67-
jsi::Value makeSerializableRemoteFunction(
66+
jsi::Value makeRNOriginSerializableRemoteFunction(
6867
jsi::Runtime &rnRuntime,
6968
const std::string &name,
7069
int remoteId,
7170
const std::shared_ptr<JSScheduler> &jsScheduler);
7271

73-
/** Creates Worklet Runtime Remote Function. */
74-
jsi::Value makeSerializableRemoteFunction(
72+
jsi::Value makeWorkletOriginSerializableRemoteFunction(
7573
jsi::Runtime &workletRuntime,
7674
const std::string &name,
7775
jsi::Function &&function,
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#include <jsi/jsi.h>
2+
#include <react/debug/react_native_assert.h>
3+
#include <worklets/SharedItems/Serializable.h>
4+
#include <worklets/SharedItems/SerializableRemoteFunction.h>
5+
#include <worklets/WorkletRuntime/RuntimeManager.h>
6+
#include <worklets/WorkletRuntime/WorkletRuntime.h>
7+
8+
#include <memory>
9+
#include <utility>
10+
11+
using namespace facebook;
12+
13+
namespace worklets {
14+
15+
namespace {
16+
17+
jsi::Function getRemoteFunctionUnpacker(jsi::Runtime &rt) {
18+
auto remoteFunctionUnpacker = rt.global().getProperty(rt, "__remoteFunctionUnpacker");
19+
react_native_assert(remoteFunctionUnpacker.isObject() && "remoteFunctionUnpacker not found");
20+
return remoteFunctionUnpacker.asObject(rt).asFunction(rt);
21+
}
22+
23+
jsi::Object getRemoteFunctionRegistry(jsi::Runtime &rt) {
24+
auto registry = rt.global().getProperty(rt, "__remoteFunctionRegistry");
25+
react_native_assert(registry.isObject() && "remoteFunctionRegistry not found");
26+
return registry.getObject(rt);
27+
}
28+
29+
} // namespace
30+
31+
SerializableRemoteFunction::~SerializableRemoteFunction() = default;
32+
33+
jsi::Value SerializableRemoteFunction::unpackSelf(jsi::Runtime &rt) {
34+
const auto nameValue = name_.empty() ? jsi::Value::undefined() : jsi::String::createFromUtf8(rt, name_);
35+
auto holderFunction = getRemoteFunctionUnpacker(rt).call(rt, nameValue).getObject(rt);
36+
holderFunction.setNativeState(rt, std::make_shared<SerializableJSRef>(shared_from_this()));
37+
return holderFunction;
38+
}
39+
40+
jsi::Value SerializableRemoteFunction::RNOrigin::toJSValue(jsi::Runtime &rt) {
41+
if (&rt == hostRuntime_) {
42+
const auto registry = getRemoteFunctionRegistry(rt);
43+
return registry.getPropertyAsFunction(rt, "get").callWithThis(rt, registry, jsi::Value(remoteId_));
44+
}
45+
std::shared_ptr<SerializableRemoteFunction> proxy;
46+
{
47+
std::lock_guard<std::mutex> lock(proxyMutex_);
48+
proxy = proxy_.lock();
49+
if (!proxy) {
50+
auto self = std::static_pointer_cast<RNOrigin>(shared_from_this());
51+
proxy = std::make_shared<RNOriginProxy>(self);
52+
proxy_ = proxy;
53+
}
54+
}
55+
return proxy->toJSValue(rt);
56+
}
57+
58+
void SerializableRemoteFunction::RNOrigin::resolveOrRejectPromise(
59+
const std::shared_ptr<Serializable> &resolveValue,
60+
const std::shared_ptr<RuntimeManager> & /*runtimeManager*/) {
61+
jsScheduler_->scheduleOnJS([resolver = shared_from_this(), resolveValue](jsi::Runtime &rt) {
62+
resolver->toJSValue(rt).getObject(rt).getFunction(rt).call(rt, resolveValue->toJSValue(rt));
63+
});
64+
}
65+
66+
SerializableRemoteFunction::WorkletOrigin::~WorkletOrigin() {
67+
cleanupRuntimeAware(hostRuntime_, function_);
68+
}
69+
70+
jsi::Value SerializableRemoteFunction::WorkletOrigin::toJSValue(jsi::Runtime &rt) {
71+
if (&rt == hostRuntime_) {
72+
return jsi::Value(rt, *function_);
73+
}
74+
return unpackSelf(rt);
75+
}
76+
77+
void SerializableRemoteFunction::WorkletOrigin::resolveOrRejectPromise(
78+
const std::shared_ptr<Serializable> &resolveValue,
79+
const std::shared_ptr<RuntimeManager> &runtimeManager) {
80+
const auto workletRuntime = runtimeManager->getRuntime(hostRuntimeId_);
81+
// NOLINTNEXTLINE(readability/braces)
82+
if (!workletRuntime) [[unlikely]] {
83+
return;
84+
}
85+
workletRuntime->schedule([resolver = shared_from_this(), resolveValue](jsi::Runtime &rt) {
86+
resolver->toJSValue(rt).getObject(rt).getFunction(rt).call(rt, resolveValue->toJSValue(rt));
87+
});
88+
}
89+
90+
SerializableRemoteFunction::RNOrigin::RNOriginProxy::RNOriginProxy(const std::shared_ptr<RNOrigin> &origin)
91+
: SerializableRemoteFunction(nullptr, origin->getHostRuntimeId(), origin->getName()), origin_(origin) {}
92+
93+
SerializableRemoteFunction::RNOrigin::RNOriginProxy::~RNOriginProxy() {
94+
origin_->getJSScheduler()->scheduleOnJS([id = origin_->getRemoteId()](jsi::Runtime &rt) {
95+
const auto registry = getRemoteFunctionRegistry(rt);
96+
registry.getPropertyAsFunction(rt, "delete").callWithThis(rt, registry, jsi::Value(id));
97+
});
98+
}
99+
100+
jsi::Value SerializableRemoteFunction::RNOrigin::RNOriginProxy::toJSValue(jsi::Runtime &rt) {
101+
if (&rt == origin_->getHostRuntime()) {
102+
return origin_->toJSValue(rt);
103+
}
104+
return unpackSelf(rt);
105+
}
106+
107+
void SerializableRemoteFunction::RNOrigin::RNOriginProxy::resolveOrRejectPromise(
108+
const std::shared_ptr<Serializable> &resolveValue,
109+
const std::shared_ptr<RuntimeManager> &runtimeManager) {
110+
origin_->resolveOrRejectPromise(resolveValue, runtimeManager);
111+
}
112+
113+
} // namespace worklets

0 commit comments

Comments
 (0)