-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
perf: avoid calling executeOnUIRuntimeSync if shared value has initial value #7913
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
base: @tjzel/worklets/synchronizable
Are you sure you want to change the base?
perf: avoid calling executeOnUIRuntimeSync if shared value has initial value #7913
Conversation
6955a68
to
eb246ef
Compare
Currently looking into making sure that in practice a race condition where the value is updated on UI thread, then read on JS before the dirty flag is passed to JS is impossible. |
Hi @janicduplessis, thanks for your contribution. This is an optimization we considered some time ago, but unfortunately it has some problems and we decided not to go further with it at the time. It can break the users' assumptions. Take a look at the following example, where the user synchronously updates the shared value: import React from 'react';
import { StyleSheet, View, Button } from 'react-native';
import { executeOnUIRuntimeSync } from 'react-native-worklets';
import Animated, {
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated';
export default function Component() {
const size = useSharedValue(100);
const [, setRenderCount] = React.useState(0);
const animatedStyle = useAnimatedStyle(() => ({
width: size.value,
height: size.value,
backgroundColor: 'blue',
}));
return (
<View style={styles.container}>
<Animated.View style={animatedStyle} />
<Button
title="Toggle"
onPress={() => {
executeOnUIRuntimeSync(() => {
size.value = 200;
})();
setRenderCount((count) => count + 1);
}}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
}); With your changes this will result in the change not applied at all, because the React Native Runtime couldn't yet know that the Shared Value was dirty: Beforeok.movAfterbad.movWhile the example might seem a bit artificial, the same interaction could happen if a gesture changed a shared value at the same moment that a component is rerendered, losing the update from the gesture. I think we could add a feature flag/another parameter to However, with some more fast-memory features that are coming to |
Hey @tjzel, thanks for the example. I thought the value from JS was only read during the first render (https://github.com/janicduplessis/react-native-reanimated/blob/eb246ef260702e48dbcf00e55088503b2151da14/packages/react-native-reanimated/src/createAnimatedComponent/PropsFilter.tsx#L91-L96), is it being read somewhere else on updates? It is def more likely to happen if it can happen on updates too. |
As for a more reliable solution what do you think about storing the dirty flag on some shared object in c++ and have the host object return that? I think keeping that dirty flag logic might be better for now to avoid having to introduce locking on the actual value. |
I was prototyping Synchronizable concept a while back but decided to postpone it. It would be a perfect fit here. The flag would be held as a C++ object with a mutex. Conceptually you'd combine your changes with: // for reading
if(!isDirty) {
isDirty = dirtyFlag.getBlocking();
}
// for writing
if(!isDirty) {
isDirty = true;
dirtyFlag.setBlocking(true);
} It should be significantly faster since the lock on the flag should be free majority of the time. By the way, the issues like in the video were one of the reasons we abandoned SyncDataHolder in favor of sync reads back in the day. |
Yes, I was thinking something similar, so we have very little locking contention. Any ideas about this point I mentionned here? #7913 (comment) |
Also any timeline on Synchronizable? Or should I look into implementing it without it and we could migrate this to use it later when its ready. |
I can't tell from the top of my head, but the case on the video might be that a render overwrites an updated style with a stale style. Would need to look into it more to recall all the shenanigans with style initializations etc.
It was pretty much close to completion but lacked some polish and testing. If you think it's a good idea to ship it asap then we could make it internally used in Shared Values in Reanimated 4.1.0 behind a feature flag. |
@tjzel Could you clarify how you reproduced this? I just tested on my branch in the repo fabric-example by replacing the content of I also double checked and the JS thread value getter is only called once on mount, so if the view is updated like in the example it doesn't has an effect. I can still see potential for a race if the view is mounted at the exact same time as the shared value is updated, but before the callback that sets isDirty is executed. I am looking to see if I can make an aritificial repro of it. |
I cleaned up the repro a bit too much, here's the consistent one. import React, { useState } from 'react';
import { StyleSheet, View, Button } from 'react-native';
import { executeOnUIRuntimeSync } from 'react-native-worklets';
import Animated, {
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated';
export default function Component() {
const size = useSharedValue(100);
const [staticSize, setStaticSize] = useState(100);
const animatedStyle = useAnimatedStyle(() => ({
width: size.value,
backgroundColor: 'blue',
}));
return (
<View style={styles.container}>
<Animated.View style={[animatedStyle, { height: staticSize }]} />
<Button
title="Toggle"
onPress={() => {
executeOnUIRuntimeSync(() => {
size.value = 200;
})();
setStaticSize(size.value);
}}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
}); |
eb246ef
to
062cbbb
Compare
062cbbb
to
5bce833
Compare
@tjzel I got it working with your Sychronizable implementation. I was able to repro the problem with my previous implementation using your last example, and can confirm this works now. If you are good with getting the Sychronizable feature out for internal usage in the current state I think this would work well. I assume we might want to split this into 2 PRs:
|
9299041
to
84e6f67
Compare
84e6f67
to
e6045da
Compare
Making a patch compatible with v3 to test more in a real app and do another round of profiling. |
I get 12ms in |
@janicduplessis I brought Btw, after some consideration this solution is still not error-free. Not locking the runtime might still get us a stale value because shared value updates are no longer "runtime transactions". ![]() We'd probably need a more elaborate example with i.e. gesture handler to document the behavior difference it induces. |
@tjzel Nice! Are we ok with that potential race condition? That version of the Synchronizable might be a bit hard for me to test since the app is still on v3, and seems like things like |
I wish we could be ok, but unfortunately no, this could break some user setups. My idea is to enable this optimization behind a feature flag so that the user opts into this behavior consciously - although it's hard to come up with a good name for it. |
Feature flag sounds good to me. |
Summary
executeOnUIRuntimeSync
is pretty slow, especially on low end android devices, and always comes up at the top when profiling apps, especially on older RN versions.The idea is to use the initial value if the shared value hasn't changed yet. When the shared value changes the first time we call into JS to set the dirty flag to true and then it will fall back to slow path calling
executeOnUIRuntimeSync
.This significantly reduces calls to
executeOnUIRuntimeSync
since most shared values are not changed before being connected to a view.Test plan
Tested by patching this in an app with heavy reanimated usage and making sure animations still work properly.
Ran profiles and seen time spend in
executeOnUIRuntimeSync
goes from ~4000ms on RN 0.74 or ~400ms on RN 0.79 to about 0.Before:
0.74
0.79
After:
No longer in the sample profile.