Skip to content
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

Fix race-condition during render #5224

Merged
merged 5 commits into from
Oct 16, 2023

Conversation

piaskowyk
Copy link
Member

Summary

This PR fixes a problem with race conditions during updating parent view and mounting children. The problem occurs in some specific circumstances:

  • Parent has an animated style
  • Parent update style without animation, just assigns new value to shared value (single frame update, without animation)
  • Child doesn't have animated style
  • Child's styles depend on parent size
  • Child is mounting during update of parent style

In this situation (repro in attached example code) may happen race conditions. See at the graph:

image

Style updating by Reanimated

image

Mounting new view by React

image

So when we merge both graph in single one, It should behave in this way:

image

But because Reanimated is trying to perform update synchronously it looks like that:

image

In this specific situation, the React Shadow Tree and UI Native Tree are not synchronized. To resolve this issue, I disabled the synchronous style update through Reanimated until those trees can be synchronized once more.

before after
Screen.Recording.2023-10-05.at.14.43.58.mov
Screen.Recording.2023-10-05.at.14.40.24.mov

Fixes #4932

Test plan

Example code
import React, { useState } from 'react';
import Reanimated, {
  useAnimatedStyle,
  useSharedValue,
} from 'react-native-reanimated';
import {Pressable, View, StyleSheet, Text} from 'react-native';

function App() {
  const [toggle, setToggle] = useState(false);
  const height = useSharedValue(100);

  const animatedStyle = useAnimatedStyle(() => {
    return { height: height.value };
  });

  return (
    <View style={styles.container}>
      <Reanimated.View style={[styles.parent, animatedStyle]}>
      {toggle && <Reanimated.View style={[styles.child]} />}
      </Reanimated.View>

      <Pressable onPress={() => {
        setToggle(!toggle);
        height.value = height.value === 100 ? 200 : 100;
      }} style={styles.button}>
        <Text>Press Me</Text>
      </Pressable>

    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    height: '100%',
  },
  parent: {
    backgroundColor: 'red',
    width: '100%',
  },
  child: {
    backgroundColor: 'green',
    height: '100%',
    width: '50%'
  },
  button: {
    position: 'absolute',
    bottom: 100,
  },
});

export default App;

@piaskowyk piaskowyk marked this pull request as ready for review October 13, 2023 07:12
Copy link
Collaborator

@tjzel tjzel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good for me, although I'm not that proficient with RN internals.

Copy link
Member

@tomekzaw tomekzaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! Thanks for explaining the idea behind this PR offline. I also don't have any comments in terms of code style ✨

Copy link
Member

@kmagiera kmagiera left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this approach much more than the previous one. However, I think it's not as intuitive as it could be and introduces back and forth coupling between swizzled uimanager and reanodesmanager.

I added one inline comment where we do a private field lookup that we could easily avoid. But I think if we redesigned this a little bit we could both get rid of that and make the communication between these two classes more easy to follow. Here is what I'd propose:

We add a method to UIManager category that's like: isExecutingUpdatesBatch or something similar. This method can be called from performOperations where we used to call hasEnqueuedUICommands. Everything else should be encapsulated inside swizzled UIManager to make this method function properly. This way we get rid of observer class and all the things around it (instantiating, assigning etc)

The method isExecutingUpdatesBatch should base its result on two variables: hasPendingBlocks and isFlushingBlocks. The first one meaning if there are any ui blocks enqueued in the batch and the second one meaning if there is a flush operations enqueued and not finished.

hasPendingBlocks can be set to true in addUIBlock and perpendUIBlock and reset back in flushUIBlocks. It will only be assigned from UI manager queue but can be accessed from UI queue.

isFlushingBlocks should be a semaphore and should be incremented in flushUIBlocks on the UImanager queue (before we reset hasPendingBlocks) and decremented in the last block that we add using addUIBlock. Note that we don't need to go through this process if hasPendingBlocks is false.

apple/LayoutReanimation/REASwizzledUIManager.mm Outdated Show resolved Hide resolved
apple/REANodesManager.mm Outdated Show resolved Hide resolved
apple/LayoutReanimation/REASwizzledUIManager.mm Outdated Show resolved Hide resolved
Copy link
Member

@kmagiera kmagiera left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is much cleaner and looks good overall. For completeness I'd add the following things:

  • add UIManager queue asserts or comments for the methods that we expect that'll only run on UI manager queue (specifically where we set hasPendingBlocks)
  • add a comment in the place where we use isExecutingUpdatesBatch that says we don't need additional synchronization beyond atomics because at the moment the method gets called, both UI manager and UI queues are blocked – this is a necessary condition for this to work w/o race

@piaskowyk piaskowyk added this pull request to the merge queue Oct 16, 2023
Merged via the queue into main with commit 0e21600 Oct 16, 2023
11 checks passed
@piaskowyk piaskowyk deleted the @piaskowyk/fix-race-condition-during-render-v2 branch October 16, 2023 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Child component does not resize properly if it remounts when parent changes dimensions
4 participants