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

Add setImmediate implementation to the UI runtime #3970

Merged
merged 26 commits into from
Feb 22, 2023
Merged

Conversation

kmagiera
Copy link
Member

Summary

This PR adds an implementation of web's setImmediate API to the reanimated's UI runtime. We utilize this implementation in mappers and event handlers for more accurate animation/gesture tracking as well as unblock further optimizations.

There are several things that this PR changes:

  1. We add event timestamp metadata to touch/scroll/sensor/etc events. This change is in preparation of providing a true system timestamps for such events (we don't do that yet but instead call system timer in order to pass down the current time)
  2. We update performance.now API to use the same reference point for the provided time as requestAnimationFrame API. Before performance.now would use a different reference point which made it impossible to use it as a starting point for animations.
  3. We update event handler callbacks such that they perform a flush for rAF and immediates queues
  4. We populate global.__frameTimestamp for all the code that executes within requestAnimationFrame and event callbacks. We then use it in useAnimatedStyle as a reference point for starting animations and in case it wasn't set, we use performance.now
  5. We add some code in initializers.ts to define setImmediate method and to register method for flushing immediates queue
  6. We introduce set-immediate-based batching for runOnUI calls on the main RN runtime. We then flush all "immediates" after the whole batch is processed on the UI runtime.

Test plan

Check a bunch of examples from the example app: "Use animated style", "Bokeh", "Drag and snap".
For touch based interaction verify that movement doesn't lag behind the event. The way this can be done is by screen recording drag and snap example with touch indicators on, then checking on the recorded clip in between frames whether there is a correct movement aligned with the updates of the touch indicator.
The above needs to be done for all configurations: Android/iOS/Fabric/Paper

@@ -323,7 +303,7 @@ void NativeProxy::installJSIBindings(
std::shared_ptr<UIManager> uiManager =
binding->getScheduler()->getUIManager();
module->setUIManager(uiManager);
module->setNewestShadowNodesRegistry(newestShadowNodesRegistry_);
module->setNewestShadowNodesRegistry(newezstShadowNodesRegistry_);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
module->setNewestShadowNodesRegistry(newezstShadowNodesRegistry_);
module->setNewestShadowNodesRegistry(newestShadowNodesRegistry_);

Comment on lines +423 to +424
std::vector<FrameCallback> callbacks = frameCallbacks;
frameCallbacks.clear();
Copy link
Contributor

Choose a reason for hiding this comment

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

You should be able to just move-construct callbacks from frameCallbacks here, right?

Suggested change
std::vector<FrameCallback> callbacks = frameCallbacks;
frameCallbacks.clear();
std::vector<FrameCallback> callbacks = std::move(frameCallbacks);

Copy link
Member Author

Choose a reason for hiding this comment

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

This wasn't my code so prefer not to make this change. Is this behavior defined that the original vector gets reset when moved? Typically move constructors does not specify behavior of the moved object as normally the assumption is that the moved object is never used again. Here we still want to use the vector

Copy link
Contributor

Choose a reason for hiding this comment

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

It should be fine.
C++ standard says that the moved-from object is left in a "valid but unspecified state" (so it should be fine to use the old vector later), and the std::vector spec further specifies that "After the move, other is guaranteed to be empty()." when move-constructing a vector, so it should clear it as well.

In case you don't feel safe using a vector after moving from it, I would still suggest doing something like

Suggested change
std::vector<FrameCallback> callbacks = frameCallbacks;
frameCallbacks.clear();
std::vector<FrameCallback> callbacks;
std::swap(callbacks, frameCallbacks)

since this is unambiguous in what it does, but still avoids copies/allocations.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd just like to avoid copying the callbacks here, since they can potentially capture a bunch of things - in this case it's a couple of shared_ptrs, which means unnecessary work on the atomic reference counters inside.

if (mappers.size !== sortedMappers.length) {
updateMappersOrder();
}
for (const mapper of sortedMappers) {
if (mapper.dirty) {
mapper.dirty = false;
mapper.worklet();
mapper.worklet(7);
Copy link
Member

Choose a reason for hiding this comment

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

Why this 7 is here?

Copy link
Member Author

Choose a reason for hiding this comment

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

idk, must've got here by mistake, mapper's worklets don't take arguments

}
_runOnUIQueue.push([worklet, args]);
if (_runOnUIQueue.length === 1) {
setImmediate(() => {
Copy link
Member

Choose a reason for hiding this comment

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

This is Reanimated or react-native setImmediate? If this is setImmediate from react-native, could we add some comment or use different naming to identify two version easily.

Copy link
Member Author

Choose a reason for hiding this comment

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

It is meant to the same setImmediate. We provide a compatibility layer with the exception that reanimated's version doesn't support cancelling callbacks (yet). So we aim for both setImmediate from RN runtime and UI runtimes to be compatible and have same behavior hence we use the standard naming

requestAnimationFrame(mapperFrame);
frameRequested = true;
if (!runRequested) {
setImmediate(mapperRun);
Copy link
Member

Choose a reason for hiding this comment

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

Why setImmediate is better than requestAnimationFrame here?

Copy link
Member Author

Choose a reason for hiding this comment

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

rAF will delay the run of the mappers by one frame – the callback will run happen in the following frame. Whereas setImmediate will execute the callback at the end of the tasks that are currently scheduled on the run loop. The reason we schedule the run in the first place is that there is a chance multiple mappers gets updated in one frame and so we want to run the updates only once as it requires traversing dependency graph.

Copy link
Contributor

@nandorojo nandorojo Mar 1, 2023

Choose a reason for hiding this comment

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

One issue with this implementation is that it's historically very problematic with Next.js, since setImmediate is not available on the server.

The current behavior leads to #4140

Would this work?

const scheduleUpdates = typeof window != 'undefined' ? global.setImmediate : requestAnimationFrame;

@kmagiera kmagiera added this pull request to the merge queue Feb 22, 2023
Merged via the queue into main with commit 535b5d6 Feb 22, 2023
@kmagiera kmagiera deleted the set-immediate branch February 22, 2023 10:21
@bglgwyng
Copy link

bglgwyng commented Feb 23, 2023

@kmagiera

I posted a question #3925 a month ago and haven't gotten any answer yet.

I just saw this PR by chance and I don't know the details and context of this PR at all. However, the quotations sound quite relevant to my previous question.

We populate global.__frameTimestamp for all the code that executes within requestAnimationFrame and event callbacks. We then use it in useAnimatedStyle as a reference point for starting animations and in case it wasn't set, we use performance.now

We introduce set-immediate-based batching for runOnUI calls on the main RN runtime. We then flush all "immediates" after the whole batch is processed on the UI runtime.

Does this change solves the problem discussed in the discussion?

@kmagiera
Copy link
Member Author

This change impacts the way we execute updates on the main thread. Now they are all batches and will run in a single transaction. So I'd expect this pr to change the way your example work but the easiest way to find out would be to just try it out.

kmagiera added a commit that referenced this pull request Feb 24, 2023
## Summary

Before this change we'd only call `performOperations` for a subset of
filtered events (initially introduced in #312). The event's that weren't
considered "direct" would require additional animation frame for having
their updates flushed. This change makes it so that we call
`performOperations` for all types of events therefore matching the
behavior [on
Android](https://github.com/software-mansion/react-native-reanimated/blob/99b8b3ed56e36ca615cce7164ccaf04d154571b1/android/src/main/java/com/swmansion/reanimated/NodesManager.java#L283)

A consequence of this change was that for some event types, the updates
reanimated was performing weren't getting flushed onto screen. Namely,
we noticed this problem in Pager example where a view pager component
with some custom set of events is used and event handlers update shared
values. Even though such event would trigger shared value update, and
these updates would trigger the style to recalculate and we'd even call
updateProps method to apply the updated props, we'd still see no result
as in that example the changes require layout run. Without
`performOperation` call the layout would not be executed unless react
would rerender or other time-based animation would run.

The problem became apparent after #3970 where we changed the place where
updates are performed from requestAnimationFrame to setImmediate. Before
this change, since we were running the updates in "animation frame" the
`performOperation` method was being run by the frame scheduler. We,
however were getting these updates delayed by one frame because of that.
This issue also wasn't noticed prior to shareable rewrite from #3722
because before, we were always starting frame updater for every single
update happening to shared value even if it was due to an event. As a
result, we were getting the stuff updated on screen but again, with a
delay of one frame.

## Test plan

Run pager example on iOS.
kmagiera added a commit that referenced this pull request Feb 24, 2023
… flow as other types of events (#4098)

## Summary

This diff fixes an issue with animated views not updating when animating
along keyboard. This problem regressed after #3970 where we changed the
logic of when enqueued callbacks for animation frame and immediates are
flushed. The logic has been moved from c++ to js and we added some code
to execute animation frame and immediates as a part of event handling
process. However, keyboard events have a separate flow of providing
event data (we should unify this at some point), and we missed updating
it in that place.

This PR adds similar logic to [what we use for regular
events](https://github.com/software-mansion/react-native-reanimated/blob/535b5d64c0720da3a9a39d99a67e878fe09b5154/src/reanimated2/core.ts#L117)
into the code responsible for handling keyboard events.

## Test plan

Run animated keyboard example on iOS and Android.
kmagiera added a commit that referenced this pull request Feb 26, 2023
## Summary

This PR fixes issue with UI not updating when style changes are
connected to events on iOS Fabric. This problem has been introduced in
#3970 where we migrated from using `rAF` and started using
`setImmediate` instead. As a result we had to manually flush
set-immediates queue in certain conditions. Specifically this was
already happening after animations frame was run (i.e. in
`requestAnimationFrame` callback) but should also be done directly after
handling events much like we already do on Android
[here](https://github.com/software-mansion/react-native-reanimated/blob/99b8b3ed56e36ca615cce7164ccaf04d154571b1/android/src/main/java/com/swmansion/reanimated/NodesManager.java#L283)
and on iOS-paper
[here](https://github.com/software-mansion/react-native-reanimated/blob/d9a55c556fc32fcb5db59acc92cbedd6452af9dc/ios/REANodesManager.mm#L334).

The reason this stopped working on iOS and not on Android was that
Android-fabric implementation still uses old architecture flow for
handling events while iOS uses new C++ based `react::EventListener` API
(that apparently wasn't working on Android last time we checked). This
PR adds flushing operations queue in that new C++ based flow.

Note that this problem didn't occur before Shareable Rewrite (#3722)
because before all mapper updates would result in us scheduling new
animation frames. So the updates were happening eventually, were just
delayed by one frame.

## Test plan

Run Fabric example on iOS, test Article progress example.
tjzel added a commit that referenced this pull request Feb 27, 2023
@eweilow
Copy link

eweilow commented Mar 1, 2023

Hi! Can create an issue as well, but I think this PR may have broken running react-native-reanimated on the Web without a Babel plugin.

The issue we are getting is that any use of react-native-reanimated throws runOnUI() can only be used on worklets coming from this line
https://github.com/software-mansion/react-native-reanimated/pull/3970/files#diff-e2f6e662599fdf6a81975be4bbd34818651984e9076077405b709d7ae6354680R96

In the PR that adds support for running without a Babel plugin (#3997), the runOnUI function is changed to add a !shouldBeUseWeb() check before testing the __workletHash:

export function runOnUI<A extends any[], R>(
  worklet: ComplexWorkletFunction<A, R>
): (...args: A) => void {
-  if (__DEV__) {
+  if (__DEV__ && !shouldBeUseWeb()) {

From what I can tell, the same needs to be done for runOnUIImmediately:

export function runOnUIImmediately<A extends any[], R>(
  worklet: ComplexWorkletFunction<A, R>
): (...args: A) => void {
-  if (__DEV__) {
+  if (__DEV__ && !shouldBeUseWeb()) {
    if (worklet.__workletHash === undefined) {

Perhaps the error message runOnUI() can only be used on worklets should also be changed to runOnUIImmediately() can only be used on worklets?

@nandorojo
Copy link
Contributor

This PR also added a regression on Next.js, which utilizes requestAnimationFrame rather than setImmediate, which isn't available on the server. I opened an issue here: #4140

nandorojo added a commit to nandorojo/react-native-reanimated that referenced this pull request Mar 1, 2023
@nandorojo
Copy link
Contributor

@eweilow Yeah you're right, I opened a PR for that fix here: #4141

piaskowyk added a commit that referenced this pull request Mar 3, 2023
Fixes
#3970 (comment)

This logic matches that of `runOnUI`.

<!-- Thanks for submitting a pull request! We appreciate you spending
the time to work on these changes. Please follow the template so that
the reviewers can easily understand what the code changes affect. -->

## Summary

<!-- Explain the motivation for this PR. Include "Fixes #<number>" if
applicable. -->

## Test plan

<!-- Provide a minimal but complete code snippet that can be used to
test out this change along with instructions how to run it and a
description of the expected behavior. -->

---------

Co-authored-by: Krzysztof Piaskowy <krzysztof.piaskowy@swmansion.com>
fluiddot pushed a commit to wordpress-mobile/react-native-reanimated that referenced this pull request Jun 5, 2023
)

## Summary

This PR adds an implementation of web's setImmediate API to the
reanimated's UI runtime. We utilize this implementation in mappers and
event handlers for more accurate animation/gesture tracking as well as
unblock further optimizations.

There are several things that this PR changes:
1) We add event timestamp metadata to touch/scroll/sensor/etc events.
This change is in preparation of providing a true system timestamps for
such events (we don't do that yet but instead call system timer in order
to pass down the current time)
2) We update performance.now API to use the same reference point for the
provided time as requestAnimationFrame API. Before performance.now would
use a different reference point which made it impossible to use it as a
starting point for animations.
3) We update event handler callbacks such that they perform a flush for
rAF and immediates queues
4) We populate global.__frameTimestamp for all the code that executes
within requestAnimationFrame and event callbacks. We then use it in
useAnimatedStyle as a reference point for starting animations and in
case it wasn't set, we use performance.now
5) We add some code in initializers.ts to define setImmediate method and
to register method for flushing immediates queue
6) We introduce set-immediate-based batching for runOnUI calls on the
main RN runtime. We then flush all "immediates" after the whole batch is
processed on the UI runtime.

## Test plan

Check a bunch of examples from the example app: "Use animated style",
"Bokeh", "Drag and snap".
For touch based interaction verify that movement doesn't lag behind the
event. The way this can be done is by screen recording drag and snap
example with touch indicators on, then checking on the recorded clip in
between frames whether there is a correct movement aligned with the
updates of the touch indicator.
The above needs to be done for all configurations:
Android/iOS/Fabric/Paper
fluiddot pushed a commit to wordpress-mobile/react-native-reanimated that referenced this pull request Jun 5, 2023
## Summary

Before this change we'd only call `performOperations` for a subset of
filtered events (initially introduced in software-mansion#312). The event's that weren't
considered "direct" would require additional animation frame for having
their updates flushed. This change makes it so that we call
`performOperations` for all types of events therefore matching the
behavior [on
Android](https://github.com/software-mansion/react-native-reanimated/blob/99b8b3ed56e36ca615cce7164ccaf04d154571b1/android/src/main/java/com/swmansion/reanimated/NodesManager.java#L283)

A consequence of this change was that for some event types, the updates
reanimated was performing weren't getting flushed onto screen. Namely,
we noticed this problem in Pager example where a view pager component
with some custom set of events is used and event handlers update shared
values. Even though such event would trigger shared value update, and
these updates would trigger the style to recalculate and we'd even call
updateProps method to apply the updated props, we'd still see no result
as in that example the changes require layout run. Without
`performOperation` call the layout would not be executed unless react
would rerender or other time-based animation would run.

The problem became apparent after software-mansion#3970 where we changed the place where
updates are performed from requestAnimationFrame to setImmediate. Before
this change, since we were running the updates in "animation frame" the
`performOperation` method was being run by the frame scheduler. We,
however were getting these updates delayed by one frame because of that.
This issue also wasn't noticed prior to shareable rewrite from software-mansion#3722
because before, we were always starting frame updater for every single
update happening to shared value even if it was due to an event. As a
result, we were getting the stuff updated on screen but again, with a
delay of one frame.

## Test plan

Run pager example on iOS.
fluiddot pushed a commit to wordpress-mobile/react-native-reanimated that referenced this pull request Jun 5, 2023
… flow as other types of events (software-mansion#4098)

## Summary

This diff fixes an issue with animated views not updating when animating
along keyboard. This problem regressed after software-mansion#3970 where we changed the
logic of when enqueued callbacks for animation frame and immediates are
flushed. The logic has been moved from c++ to js and we added some code
to execute animation frame and immediates as a part of event handling
process. However, keyboard events have a separate flow of providing
event data (we should unify this at some point), and we missed updating
it in that place.

This PR adds similar logic to [what we use for regular
events](https://github.com/software-mansion/react-native-reanimated/blob/535b5d64c0720da3a9a39d99a67e878fe09b5154/src/reanimated2/core.ts#L117)
into the code responsible for handling keyboard events.

## Test plan

Run animated keyboard example on iOS and Android.
fluiddot pushed a commit to wordpress-mobile/react-native-reanimated that referenced this pull request Jun 5, 2023
## Summary

This PR fixes issue with UI not updating when style changes are
connected to events on iOS Fabric. This problem has been introduced in
software-mansion#3970 where we migrated from using `rAF` and started using
`setImmediate` instead. As a result we had to manually flush
set-immediates queue in certain conditions. Specifically this was
already happening after animations frame was run (i.e. in
`requestAnimationFrame` callback) but should also be done directly after
handling events much like we already do on Android
[here](https://github.com/software-mansion/react-native-reanimated/blob/99b8b3ed56e36ca615cce7164ccaf04d154571b1/android/src/main/java/com/swmansion/reanimated/NodesManager.java#L283)
and on iOS-paper
[here](https://github.com/software-mansion/react-native-reanimated/blob/d9a55c556fc32fcb5db59acc92cbedd6452af9dc/ios/REANodesManager.mm#L334).

The reason this stopped working on iOS and not on Android was that
Android-fabric implementation still uses old architecture flow for
handling events while iOS uses new C++ based `react::EventListener` API
(that apparently wasn't working on Android last time we checked). This
PR adds flushing operations queue in that new C++ based flow.

Note that this problem didn't occur before Shareable Rewrite (software-mansion#3722)
because before all mapper updates would result in us scheduling new
animation frames. So the updates were happening eventually, were just
delayed by one frame.

## Test plan

Run Fabric example on iOS, test Article progress example.
fluiddot pushed a commit to wordpress-mobile/react-native-reanimated that referenced this pull request Jun 5, 2023
Fixes
software-mansion#3970 (comment)

This logic matches that of `runOnUI`.

<!-- Thanks for submitting a pull request! We appreciate you spending
the time to work on these changes. Please follow the template so that
the reviewers can easily understand what the code changes affect. -->

## Summary

<!-- Explain the motivation for this PR. Include "Fixes #<number>" if
applicable. -->

## Test plan

<!-- Provide a minimal but complete code snippet that can be used to
test out this change along with instructions how to run it and a
description of the expected behavior. -->

---------

Co-authored-by: Krzysztof Piaskowy <krzysztof.piaskowy@swmansion.com>
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.

6 participants