-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
chore: Replace view descriptors set with view descriptors map #8324
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: main
Are you sure you want to change the base?
Conversation
RNScreens: 6ced6ae8a526512a6eef6e28c2286e1fc2d378c3 | ||
RNSVG: 287504b73fa0e90a605225aa9f852a86d5461e84 | ||
RNWorklets: 7119ae08263033c456c80d90794a312f2f88c956 | ||
RNWorklets: 991f94e4fa31fc20853e74d5d987426f8580cb0d |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These changed when I installed pods. Not sure if it'd better to add these changes in a separate PR instead.
string(APPEND CMAKE_CXX_FLAGS " -fno-omit-frame-pointer -fstack-protector-all") | ||
# flags to optimize the binary size | ||
string(APPEND CMAKE_CXX_FLAGS " -fvisibility=hidden -ffunction-sections -fdata-sections") | ||
string(APPEND CMAKE_CXX_FLAGS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here as well, I ran the yarn format
command and this has changed.
if (!cachedArray.value) { | ||
cachedArray.value = Array.from(sharedDescriptors.value.values()); | ||
} | ||
return cachedArray.value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if I should use the .modify
method here. I think I don't have to as I am never modifying the already stored value (I always assign a new one), so the .value
assignment seems to be fine.
updateProps = (viewDescriptors, updates, isAnimatedProps) => { | ||
'worklet'; | ||
viewDescriptors.value?.forEach((viewDescriptor) => { | ||
for (const viewDescriptor of viewDescriptors.toArray()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for ... of
loops are faster than forEach
so I decided to change them as well as a part of the view descriptors optimization.
|
||
return { | ||
shareableViewDescriptors: { | ||
...(sharedDescriptors as SharedValue<ReadonlyMap<ViewTag, Descriptor>>), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you spread sharedDescriptors
mutable here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because I want to add an additional field to it - the toArray
method. It allowed me to get rid of type casting in the packages/react-native-reanimated/src/screenTransition/styleUpdater.ts
file and to easily get the array (usually cached) from the SharedValue via viewDescriptors.toArray()
in the packages/react-native-reanimated/src/updateProps/updateProps.ts
.
And, since the Mutable is a JS object, extending it in this way seems to be fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's safe. Mutables use the reference from makeMutable
so spreading it might lead to undefined behavior. It'd be better to call Object.defineProperty
instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both implementations of makeMutable
(i.e. makeMutableNative
and makeMutableWeb
) create a mutable object, which is a plain JS object so I think my approach is correct. We don't reference the mutable itself via this
in the mutable object methods and every method is just a function which should still be able to use values from its closure after being destructed.
I can change it to Object.defineProperty
if you prefer but it shouldn't change the behavior (at least I can't see how it may change it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We call serializableMappingCache.set(mutable, handle);
. I'd rather use Object.defineProperty
for safety.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with Tomek, it is safer to avoid a spread operator in that case.
|
||
return { | ||
shareableViewDescriptors: { | ||
...(sharedDescriptors as SharedValue<ReadonlyMap<ViewTag, Descriptor>>), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with Tomek, it is safer to avoid a spread operator in that case.
|
||
export function makeViewDescriptorsMap(): ViewDescriptorsMap { | ||
const sharedDescriptors = makeMutable<Map<ViewTag, Descriptor>>(new Map()); | ||
const cachedArray = makeMutable<Descriptor[] | null>(null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious about the performance of this new solution with a cacheable array. Usually, the view descriptors container doesn't contain more than 3 elements (mostly just one) So, finding the index with linear complexity isn't too bad in this context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if we have a FlatList with hundreds of elements and each of them share the same animated style? It would register/unregister the style quite often and doing it in O(n)
for each of elements doesn't seem to be performant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, it could be problematic in this cases, but it's not the primary use case. I just want to ensure that this doesn't introduce a performance regression when we have many components, each with its own style (not shared).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I see. The map is used only during renders, so the performance difference might be noticeable only during renders, not on every animations frame, so I think it is not that bad. Animations still use the cached array, which is invalidated only when new animated styles are added or old are removed (very rare cases).
Wrapping up, it shouldn't affect the performance of animations but may affect the performance of renders if we have multiple views with separate animated styles each (still I feel that this performance loss shouldn't be that much noticeable but I haven't measured the impact).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's another case to consider. Let's imagine a long FlatList where every component uses the same animated style. The animation is playing (for example skeleton animation). As we scroll down, we'll create new animated components, and each time we have to recreate the whole array. Maybe we can replace the recreation of an array with simply pushing new descriptor to it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that it usually works in such a way that new elements are rendered but old ones are removed at the same time when we scroll the list. In this case, pushing won't give us any benefit as we won't be able to perform a fast removal of elements, so the array would have to still be re-created.
The old implementation re-created it on each .remove()
call made to the ViewDescriptorsSet
, so for n
elements removed, its time complexity was O(n^2)
. Now, all updates are batched until the next animation frame is executed. Thanks to this, we will perform all removals before the array is re-created so it is still better than it was.
If we consider only additions, then yes, we could optimize it a bit by pushing new descriptors to the array but I think it is a very rare case to have more elements added without old ones being removed.
sharedDescriptors.modify((descriptors) => { | ||
'worklet'; | ||
descriptors.set(item.tag, item); | ||
cachedArray.value = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why we can just push to an array instead of create new one?
|
||
export function makeViewDescriptorsMap(): ViewDescriptorsMap { | ||
const sharedDescriptors = makeMutable<Map<ViewTag, Descriptor>>(new Map()); | ||
const cachedArray = makeMutable<Descriptor[] | null>(null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's another case to consider. Let's imagine a long FlatList where every component uses the same animated style. The animation is playing (for example skeleton animation). As we scroll down, we'll create new animated components, and each time we have to recreate the whole array. Maybe we can replace the recreation of an array with simply pushing new descriptor to it?
Summary
This PR replaces the old
ViewDescriptorsSet
implementation with theViewDescriptorsMap
. The main difference is that it uses aMap
object to store view descriptors instead of storing them in the array. Thanks to this, now theadd
andremove
methods haveO(1)
time complexity compared to the previousO(n)
time complexity.Because the iteration over a
Map
via the map.values()
iterator is slower than the iteration over an array (because an array uses a continuous memory, thus the iteration over its items is well-optimized compared to the Map's iterator iteration that uses pointers to subsequent elements), I decided to add thetoArray
method on theShareableViewDescriptors
object, which is basically an extension of theSharedValue
type. This conversion happens only when the previous array was invalidated because of the.add()
or.remove()
method call on theViewDescriptorsMap
. In all other cases, the already cached array is used.