Async JSI functions with promises block the event loop indefinitely
See original GitHub issueDescription
While playing with the JSI I have found that using Promises in conjunction with async JSI functions can lead to the JS thread being blocked. According to the react native profiler the JS thread is still operating at 60fps but the JS thread is not responding to tap handlers and never resolves the promise. Note this happens sometimes and is hard to predict which suggests there is some race condition.
I am able to unblock the JS thread by periodically kicking the event loop into action with something like
const tick = async () => {
await new Promise(r => setTimeout(r, 1000))
tick()
}
tick()
I’ve also noticed that using Promises in conjunction with the JSI can occasionally lead to extremely long await times for the promise to be resolved, in the order of 250ms.
I have only tested this on iOS without Hermes. Occurs on both simulator and device.
Version
0.67.1
Output of npx react-native info
System:
OS: macOS 12.0.1
CPU: (10) x64 Apple M1 Max
Memory: 20.13 MB / 32.00 GB
Shell: 5.8 - /bin/zsh
Binaries:
Node: 14.18.2 - ~/.nvm/versions/node/v14.18.2/bin/node
Yarn: 1.22.17 - ~/.nvm/versions/node/v14.18.2/bin/yarn
npm: 6.14.6 - ~/.nvm/versions/node/v14.18.2/bin/npm
Watchman: 2021.12.20.00 - /usr/local/bin/watchman
Managers:
CocoaPods: 1.11.2 - /usr/local/bin/pod
SDKs:
iOS SDK:
Platforms: DriverKit 21.0.1, iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0
Android SDK: Not Found
IDEs:
Android Studio: Not Found
Xcode: 13.1/13A1030d - /usr/bin/xcodebuild
Languages:
Java: Not Found
Python: 2.7.18 - /usr/bin/python
npmPackages:
@react-native-community/cli: Not Found
react: 16.13.1 => 16.13.1
react-native: 0.63.4 => 0.63.4
react-native-macos: Not Found
npmGlobalPackages:
*react-native*: Not Found
Steps to reproduce
A minimal failing example can be found here https://github.com/mfbx9da4/react-native-jsi-promise
1
Create an async native JSI function which spawns a thread, does some work and then resolves once it’s done its work. You can use a callback or a promise to resolve from the native function. I’ve opted to use a callback as there are less moving parts this way. You could also use std::thread
instead of GCD’s dispatch_async
for spawning the thread, the behaviour is observed with both. I’ve opted to use dispatch_async
below.
auto foo = jsi::Function::createFromHostFunction(
jsiRuntime,
jsi::PropNameID::forAscii(jsiRuntime, "foo"),
1,
[cryptoPp, myqueue](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value {
auto userCallbackRef = std::make_shared<jsi::Object>(arguments[0].getObject(runtime));
dispatch_async(myqueue, ^(void){
auto val = jsi::String::createFromUtf8(runtime, std::to_string(std::rand()));
auto error = jsi::Value::undefined();
userCallbackRef->asFunction(runtime).call(runtime, error, val);
});
return jsi::Value::undefined();
}
);
jsiRuntime.global().setProperty(jsiRuntime, "foo", std::move(foo));
2
Call the JSI function in a loop resolving the promise
for (let i = 0; i < 10; i++) {
const start = Date.now();
await new Promise(r => {
jsiPromise.foo((err, x) => {
console.log(i, 'err, x', err, x, Date.now() - start);
r(x);
});
});
}
It might take several refreshes to get stuck but the JS thread does eventually get stuck.
Snack, code example, screenshot, or link to a repository
Issue Analytics
- State:
- Created 2 years ago
- Reactions:7
- Comments:16 (10 by maintainers)
To provide an update on this, I found that we were able to expose the
jsCallInvoker
member of theRCTBridge
, which has a methodinvokeAsync
which calls a lambda you pass to it on the JS thread, then callsflush()
. This allows us to flush the UI queue without needing to modify React Native to exposeJSIExecutor::flush()
directly.The lambda to
invokeAsync
could just be a no-op, but in our case I’m using to reset a flag which prevents us making multiple calls toinvokeAsync
if one is already pending (e.g. if many C++ to JS calls occur in a short space of time).You can see how we’ve implemented this for Realm in this PR, essentially we create a
std::function
which callsinvokeAsync
and then pass this through our initialization code so that our “call JS function” abstraction (we abstract over multiple Javascript engines) can call it whenever we call into JS from C++.I’d be interested to know if there is a more “official” solution for this scenario planned though!
It looks like you call that method from a different thread. I guess you should schedule that callback to JS thread with Jscallinvoker which if I remember correctly is keeping count of tasks like that.