question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Async JSI functions with promises block the event loop indefinitely

See original GitHub issue

Description

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));

Source code here

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);
      });
    });
  }

Source code here

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

https://github.com/mfbx9da4/react-native-jsi-promise

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:7
  • Comments:16 (10 by maintainers)

github_iconTop GitHub Comments

4reactions
tomduncalfcommented, Apr 26, 2022

To provide an update on this, I found that we were able to expose the jsCallInvoker member of the RCTBridge, which has a method invokeAsync which calls a lambda you pass to it on the JS thread, then calls flush(). This allows us to flush the UI queue without needing to modify React Native to expose JSIExecutor::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 to invokeAsync 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 calls invokeAsync 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!

3reactions
Szymon20000commented, Mar 1, 2022

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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Understanding the Event Loop, Callbacks, Promises, and ...
An async function allows you to handle asynchronous code in a manner that appears synchronous. async functions still use promises under the hood ......
Read more >
Node.js Event-Loop: How even quick Node.js async functions ...
I recently stumbled upon a real event-loop blocking scenario here at Snyk. When I tried to fix the situation, I realized how little...
Read more >
React Native - when JS is too busy - DEV Community ‍ ‍
When the execution of a block of code is completed, the event loop checks on the queue if there are some future results...
Read more >
Why is my infinite loop blocking when it is in an async function?
The async keyword, and promises in general, don't make synchronous code asynchronous, slow running code fast, or blocking code non-blocking.
Read more >
React native how to return a string from a function-React Native
React Native JSI: How to call any javascript function from native? Get string result from function React Native · Return data from Async...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found