-
Notifications
You must be signed in to change notification settings - Fork 24.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Async JSI functions with promises block the event loop indefinitely #33006
Comments
Does this happen on JSC as well as on Hermes? |
Only tested JSC and iOS thus far but present on device and simulator. I have now updated the description. |
Hey @mfbx9da4, I believe you've already seen how we approached fixing a similar issue in Realm: realm/realm-js#4330. In our case, we'd see the UI being out of sync after any kind of async update had happened. As soon as you touched the screen, the UI would update. I'm not sure if that's exactly the same as what you are seeing, but it sounds pretty similar. Hopefully my description on our issue is clear – in essence it seems that React Native queues up async work created by e.g. Because we bypass the React Native bridge in Realm, and instead work directly with the JavaScript engine, when an async function call from our native code to JS happened, this did not hit any React Native codepath, and so RN had no way of knowing it should flush this queue as a result of the change. I guess your situation could be similar, I wasn't quite sure what I was looking for in your sample app, both buttons seemed to work OK. We are currently thinking we will go with an approach of manually calling I'd be interested to know if anyone has any other thoughts on more elegant ways to solve this though! |
Thanks for flagging this. This is a great question. We also realized the original bridge-based microtask mechanism wouldn't work for sync C++ -> JS call in the new architecture. I added That being said,
So...I think it's about right to directly call If it became a norm, I think it may make sense to expose a I'll keep you posted whenever I have a update, but also feel free to follow-up! |
Hey @Huxpro , thanks so much for the detailed answer and it’s great to know we were on the right track! We implemented the solution of calling I also tried implementing a solution where we send a “dummy” message over the bridge with every C++ -> JS update, using the usual native modules method, and this works perfectly, but feels a little inelegant. I was wondering whether there might be more going on under the hood when you send a bridge message, i.e. we are missing some other important part when we just call I will experiment a little and report back. Thanks for the input! |
Thanks for all your input on this. Sorry I haven't responded yet, I haven't got round to testing the proposed solutions. For now, I'm avoiding using the JSI for asynchronous methods. @Huxpro could I get a link to the |
I've seen that and I'm not sure if that's an expected behavior or not. Actually, I think instead of calling I'll try to loop in someone understanding this better. |
It looks like you call that method from a different thread. |
@Huxpro Thanks for the pointer to try calling However, I couldn't work out a way to call Regarding "I don't understand why TurboModule aren't flushing things properly automatically for you" – we are calling directly from C++ to JS, using JSC’s While this is probably a slightly unusual pattern for a React Native module to communicate to JS right now, our understanding is that with the new architecture, this kind of direct C++ to JS communication will become more commonplace, so others may hit the same issues – or is the intention that libraries still use the EventEmitter pattern with TurboModules? |
To provide an update on this, I found that we were able to expose the The lambda to You can see how we've implemented this for Realm in this PR, essentially we create a I'd be interested to know if there is a more "official" solution for this scenario planned though! |
This issue is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 7 days. |
Although we have a workaround, I still feel this issue is important to get a proper fix for. |
@kraenhansen does this still happen in 0.70? We'll be releasing the first RC of 71 over the next couple weeks, might be worth checking out if it's still the case on that one |
+1 to this. Also for anything New Architecture related like this one, posting on reactwg/react-native-new-architecture should be the preferred way as we're getting a lot of noise on the React Native issue tracker, causing issue like this one to get lost. |
What should I do In JNI code |
@310381453 here is how I did it for Android: PR. Some of the complexity may be related to how Realm is set up. I seem to recall that if you use Turbomodules it is easier to get hold of the async call invoker. |
@kelset we're still seeing this on RN 0.71.0 (just tested with 0.71.0-rc.5 from our |
As mentioned by @Szymon20000, this could be a problem in your code example @mfbx9da4. JS runtime can only be accessed from single thread at a time. Access from multiple threads is undefined behaviour. Could you fix it and see if you can still repro the problem? |
Since this issue is a big problem for us because this behavior blocks our callbacks for push messages if any setTimeout is currently running, we implemented some workaround. We built a timer hook that calls setTimeout without the milliesconds parameter periodically, hence it releases the javascripts macro task queue after each execution. In this function we check if the time elapsed since the timer starts. Hope this workaround helps for all having this issue. Hook implementation: import { useCallback, useRef } from 'react';
/**
* Props for the useTimeout hook.
*/
interface UseTimeoutProps {
// number of milliseconds after that the callback is called
timeout: number;
// The callback that is called after the timeout exceeds
callback: () => void;
}
/**
* Hook that provides a setTimeout alternative to provide a workaround for the issue https://github.com/facebook/react-native/issues/33006 .
* The callback given by the props is called after the timeout given by the props (in milliseconds) exceeds.
* The hook returns a start and stop function that can be used to start and stop the timer.
*
* @param props
*/
export const useTimeout = (props: UseTimeoutProps) => {
const startDate = useRef<Date | null>(null);
/**
* Checks if the timeout was reached. If yes, the callback will be called. If no,
* the method will queue itself to the JavaScript macro tasks to check again.
*/
const macroTaskRun = useCallback(() => {
if (!startDate.current) {
return;
}
const tmpStartDate = new Date(startDate.current);
tmpStartDate.setMilliseconds(tmpStartDate.getMilliseconds() + props.timeout);
if (new Date() >= tmpStartDate) {
props.callback();
} else {
setTimeout(macroTaskRun);
}
}, [props]);
/**
* Starts the timer.
*/
const start = useCallback(() => {
startDate.current = new Date();
macroTaskRun();
}, [macroTaskRun]);
/**
* Stops the timer.
*/
const stop = useCallback(() => {
startDate.current = null;
}, []);
return {
start,
stop,
};
}; usage: ...
const timeoutCallback = useCallback(() => console.log('finished'), [])
const { start, stop } = useTimeout({ timeout: 20000, callback: timeoutCallback });
useEffect(() => {
start();
}, []);
useEffect(() => {
stop();
}, [someStopCondition]);
... |
Good news: In an attempt to reproduce the behaviour through a C++ native module in the RN Tester app, I finally realized the culprit of our issue. We were relying on platform primitives, such as the CoreFoundation RunLoop (on iOS) and ALooper on Android to schedule async work. As outlined in the description of realm/realm-js#6791 we should instead schedule work trough the React Native CallInvoker API. I consider this issue closed from the perspective of the |
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
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
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'sdispatch_async
for spawning the thread, the behaviour is observed with both. I've opted to usedispatch_async
below.Source code here
2
Call the JSI function in a loop resolving the promise
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
The text was updated successfully, but these errors were encountered: