Skip to content

Commit

Permalink
Add setImmediate implementation to the UI runtime (#3970)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
kmagiera authored Feb 22, 2023
1 parent 131eb84 commit 535b5d6
Show file tree
Hide file tree
Showing 29 changed files with 276 additions and 225 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/static-root-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v2
- name: Use Node.js 14
- name: Use Node.js 16
uses: actions/setup-node@v2
with:
node-version: 14
node-version: 16
cache: 'yarn'
- name: Install node dependencies
run: yarn
Expand Down
10 changes: 6 additions & 4 deletions Common/cpp/AnimatedSensor/AnimatedSensorModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ jsi::Value AnimatedSensorModule::registerSensor(
}

auto &rt = *runtimeHelper->uiRuntime();
auto handler =
shareableHandler->getJSValue(rt).asObject(rt).asFunction(rt);
auto handler = shareableHandler->getJSValue(rt);
if (sensorType == SensorType::ROTATION_VECTOR) {
jsi::Object value(rt);
// TODO: timestamp should be provided by the platform implementation
// such that the native side has a chance of providing a true event
// timestamp
value.setProperty(rt, "qx", newValues[0]);
value.setProperty(rt, "qy", newValues[1]);
value.setProperty(rt, "qz", newValues[2]);
Expand All @@ -54,14 +56,14 @@ jsi::Value AnimatedSensorModule::registerSensor(
value.setProperty(rt, "pitch", newValues[5]);
value.setProperty(rt, "roll", newValues[6]);
value.setProperty(rt, "interfaceOrientation", orientationDegrees);
handler.call(rt, value);
runtimeHelper->runOnUIGuarded(handler, value);
} else {
jsi::Object value(rt);
value.setProperty(rt, "x", newValues[0]);
value.setProperty(rt, "y", newValues[1]);
value.setProperty(rt, "z", newValues[2]);
value.setProperty(rt, "interfaceOrientation", orientationDegrees);
handler.call(rt, value);
runtimeHelper->runOnUIGuarded(handler, value);
}
});
if (sensorId != -1) {
Expand Down
57 changes: 17 additions & 40 deletions Common/cpp/NativeModules/NativeReanimatedModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -309,11 +309,13 @@ jsi::Value NativeReanimatedModule::registerEventHandler(

scheduler->scheduleOnUI([=] {
jsi::Runtime &rt = *runtimeHelper->uiRuntime();
auto handlerFunction =
handlerShareable->getJSValue(rt).asObject(rt).asFunction(rt);
auto handlerFunction = handlerShareable->getJSValue(rt);
auto handler = std::make_shared<WorkletEventHandler>(
newRegistrationId, eventName, std::move(handlerFunction));
eventHandlerRegistry->registerEventHandler(handler);
runtimeHelper,
newRegistrationId,
eventName,
std::move(handlerFunction));
eventHandlerRegistry->registerEventHandler(std::move(handler));
});

return jsi::Value(static_cast<double>(newRegistrationId));
Expand Down Expand Up @@ -393,19 +395,11 @@ jsi::Value NativeReanimatedModule::configureLayoutAnimation(
}

void NativeReanimatedModule::onEvent(
double eventTimestamp,
const std::string &eventName,
const jsi::Value &payload) {
try {
eventHandlerRegistry->processEvent(*runtime, eventName, payload);
} catch (std::exception &e) {
std::string str = e.what();
this->errorHandler->setError(str);
this->errorHandler->raise();
} catch (...) {
std::string str = "OnEvent error";
this->errorHandler->setError(str);
this->errorHandler->raise();
}
eventHandlerRegistry->processEvent(
*runtime, eventTimestamp, eventName, payload);
}

bool NativeReanimatedModule::isAnyHandlerWaitingForEvent(
Expand All @@ -421,20 +415,10 @@ void NativeReanimatedModule::maybeRequestRender() {
}

void NativeReanimatedModule::onRender(double timestampMs) {
try {
std::vector<FrameCallback> callbacks = frameCallbacks;
frameCallbacks.clear();
for (auto &callback : callbacks) {
callback(timestampMs);
}
} catch (std::exception &e) {
std::string str = e.what();
this->errorHandler->setError(str);
this->errorHandler->raise();
} catch (...) {
std::string str = "OnRender error";
this->errorHandler->setError(str);
this->errorHandler->raise();
std::vector<FrameCallback> callbacks = frameCallbacks;
frameCallbacks.clear();
for (auto &callback : callbacks) {
callback(timestampMs);
}
}

Expand Down Expand Up @@ -485,13 +469,7 @@ bool NativeReanimatedModule::handleEvent(
const std::string &eventName,
const jsi::Value &payload,
double currentTime) {
jsi::Runtime &rt = *runtime.get();
jsi::Object global = rt.global();
jsi::String eventTimestampName =
jsi::String::createFromAscii(rt, "_eventTimestamp");
global.setProperty(rt, eventTimestampName, currentTime);
onEvent(eventName, payload);
global.setProperty(rt, eventTimestampName, jsi::Value::undefined());
onEvent(currentTime, eventName, payload);

// TODO: return true if Reanimated successfully handled the event
// to avoid sending it to JavaScript
Expand Down Expand Up @@ -674,13 +652,12 @@ jsi::Value NativeReanimatedModule::subscribeForKeyboardEvents(
const jsi::Value &handlerWorklet,
const jsi::Value &isStatusBarTranslucent) {
auto shareableHandler = extractShareableOrThrow(rt, handlerWorklet);
auto uiRuntime = runtimeHelper->uiRuntime();
return subscribeForKeyboardEventsFunction(
[=](int keyboardState, int height) {
jsi::Runtime &rt = *uiRuntime;
jsi::Runtime &rt = *runtimeHelper->uiRuntime();
auto handler = shareableHandler->getJSValue(rt);
handler.asObject(rt).asFunction(rt).call(
rt, jsi::Value(keyboardState), jsi::Value(height));
runtimeHelper->runOnUIGuarded(
handler, jsi::Value(keyboardState), jsi::Value(height));
},
isStatusBarTranslucent.getBool());
}
Expand Down
5 changes: 4 additions & 1 deletion Common/cpp/NativeModules/NativeReanimatedModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ class NativeReanimatedModule : public NativeReanimatedModuleSpec,

void onRender(double timestampMs);

void onEvent(const std::string &eventName, const jsi::Value &payload);
void onEvent(
double eventTimestamp,
const std::string &eventName,
const jsi::Value &payload);

bool isAnyHandlerWaitingForEvent(std::string eventName);

Expand Down
3 changes: 2 additions & 1 deletion Common/cpp/Registries/EventHandlerRegistry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ void EventHandlerRegistry::unregisterEventHandler(uint64_t id) {

void EventHandlerRegistry::processEvent(
jsi::Runtime &rt,
double eventTimestamp,
const std::string &eventName,
const jsi::Value &eventPayload) {
std::vector<std::shared_ptr<WorkletEventHandler>> handlersForEvent;
Expand All @@ -40,7 +41,7 @@ void EventHandlerRegistry::processEvent(
eventPayload.asObject(rt).setProperty(
rt, "eventName", jsi::String::createFromUtf8(rt, eventName));
for (auto handler : handlersForEvent) {
handler->process(rt, eventPayload);
handler->process(eventTimestamp, eventPayload);
}
}

Expand Down
1 change: 1 addition & 0 deletions Common/cpp/Registries/EventHandlerRegistry.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class EventHandlerRegistry {

void processEvent(
jsi::Runtime &rt,
double eventTimestamp,
const std::string &eventName,
const jsi::Value &eventPayload);

Expand Down
39 changes: 14 additions & 25 deletions Common/cpp/Tools/RuntimeDecorator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,28 +71,6 @@ void RuntimeDecorator::decorateRuntime(
#endif // DEBUG

jsi_utils::installJsiFunction(rt, "_log", logValue);

auto chronoNow = [](jsi::Runtime &rt,
const jsi::Value &thisValue,
const jsi::Value *args,
size_t count) -> jsi::Value {
double now = std::chrono::system_clock::now().time_since_epoch() /
std::chrono::milliseconds(1);
return jsi::Value(now);
};

rt.global().setProperty(
rt,
"_chronoNow",
jsi::Function::createFromHostFunction(
rt, jsi::PropNameID::forAscii(rt, "_chronoNow"), 0, chronoNow));
jsi::Object performance(rt);
performance.setProperty(
rt,
"now",
jsi::Function::createFromHostFunction(
rt, jsi::PropNameID::forAscii(rt, "now"), 0, chronoNow));
rt.global().setProperty(rt, "performance", performance);
}

void RuntimeDecorator::decorateUIRuntime(
Expand Down Expand Up @@ -144,9 +122,20 @@ void RuntimeDecorator::decorateUIRuntime(
jsi_utils::installJsiFunction(
rt, "_updateDataSynchronously", updateDataSynchronously);

jsi_utils::installJsiFunction(rt, "_getCurrentTime", getCurrentTime);
rt.global().setProperty(rt, "_frameTimestamp", jsi::Value::undefined());
rt.global().setProperty(rt, "_eventTimestamp", jsi::Value::undefined());
auto performanceNow = [getCurrentTime](
jsi::Runtime &rt,
const jsi::Value &thisValue,
const jsi::Value *args,
size_t count) -> jsi::Value {
return jsi::Value(getCurrentTime());
};
jsi::Object performance(rt);
performance.setProperty(
rt,
"now",
jsi::Function::createFromHostFunction(
rt, jsi::PropNameID::forAscii(rt, "now"), 0, performanceNow));
rt.global().setProperty(rt, "performance", performance);

// layout animation
jsi_utils::installJsiFunction(
Expand Down
5 changes: 3 additions & 2 deletions Common/cpp/Tools/WorkletEventHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
namespace reanimated {

void WorkletEventHandler::process(
jsi::Runtime &rt,
double eventTimestamp,
const jsi::Value &eventValue) {
handler.callWithThis(rt, handler, eventValue);
_runtimeHelper->runOnUIGuarded(
_handlerFunction, jsi::Value(eventTimestamp), eventValue);
}

} // namespace reanimated
16 changes: 12 additions & 4 deletions Common/cpp/Tools/WorkletEventHandler.h
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#pragma once

#include <jsi/jsi.h>
#include <memory>
#include <string>
#include <utility>

#include "Shareables.h"

using namespace facebook;

namespace reanimated {
Expand All @@ -14,17 +17,22 @@ class WorkletEventHandler {
friend EventHandlerRegistry;

private:
std::shared_ptr<JSRuntimeHelper> _runtimeHelper;
uint64_t id;
std::string eventName;
jsi::Function handler;
jsi::Value _handlerFunction;

public:
WorkletEventHandler(
const std::shared_ptr<JSRuntimeHelper> &runtimeHelper,
uint64_t id,
std::string eventName,
jsi::Function &&handler)
: id(id), eventName(eventName), handler(std::move(handler)) {}
void process(jsi::Runtime &rt, const jsi::Value &eventValue);
jsi::Value &&handlerFunction)
: _runtimeHelper(runtimeHelper),
id(id),
eventName(eventName),
_handlerFunction(std::move(handlerFunction)) {}
void process(double eventTimestamp, const jsi::Value &eventValue);
};

} // namespace reanimated
17 changes: 15 additions & 2 deletions __tests__/Animation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ const getDefaultStyle = () => ({
margin: 30,
});

const originalAdvanceTimersByTime = jest.advanceTimersByTime;

jest.advanceTimersByTime = (timeMs) => {
// This is a workaround for an issue with using setImmediate that's in the jest
// environment implemented as a 0-second timeout. Because of the fact we use
// setImmediate for scheduling runOnUI tasks as well as executing matters,
// starting new animaitons gets delayed be three frames. To compensate for that
// we execute pending timers three times before advancing the timers.
jest.runOnlyPendingTimers();
jest.runOnlyPendingTimers();
jest.runOnlyPendingTimers();
originalAdvanceTimersByTime(timeMs);
};

describe('Tests of animations', () => {
beforeEach(() => {
jest.useFakeTimers();
Expand Down Expand Up @@ -95,7 +109,7 @@ describe('Tests of animations', () => {

fireEvent.press(button);
jest.advanceTimersByTime(250);
jest.runOnlyPendingTimers(); // timers scheduled for the exact 250ms won't run without this additional call

style.width = 50; // value of component width after 150ms of animation
expect(view).toHaveAnimatedStyle(style);
});
Expand All @@ -109,7 +123,6 @@ describe('Tests of animations', () => {

fireEvent.press(button);
jest.advanceTimersByTime(250);
jest.runOnlyPendingTimers();
style.width = 50; // value of component width after 250ms of animation
expect(view).toHaveAnimatedStyle(style, true);
});
Expand Down
16 changes: 14 additions & 2 deletions __tests__/InterpolateColor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ import Animated, {
withTiming,
} from '../src';

const originalAdvanceTimersByTime = jest.advanceTimersByTime;

jest.advanceTimersByTime = (timeMs) => {
// This is a workaround for an issue with using setImmediate that's in the jest
// environment implemented as a 0-second timeout. Because of the fact we use
// setImmediate for scheduling runOnUI tasks as well as executing matters,
// starting new animaitons gets delayed be three frames. To compensate for that
// we execute pending timers three times before advancing the timers.
jest.runOnlyPendingTimers();
jest.runOnlyPendingTimers();
jest.runOnlyPendingTimers();
originalAdvanceTimersByTime(timeMs);
};

describe('colors interpolation', () => {
it('interpolates rgb without gamma correction', () => {
const colors = ['#105060', '#609020'];
Expand Down Expand Up @@ -157,15 +171,13 @@ describe('colors interpolation', () => {

fireEvent.press(button);
jest.advanceTimersByTime(250);
jest.runOnlyPendingTimers();

expect(view).toHaveAnimatedStyle(
{ backgroundColor: 'rgba(71, 117, 73, 1)' },
true
);

jest.advanceTimersByTime(250);
jest.runOnlyPendingTimers();

expect(view).toHaveAnimatedStyle(
{ backgroundColor: 'rgba(96, 144, 32, 1)' },
Expand Down
19 changes: 2 additions & 17 deletions android/src/main/cpp/NativeProxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,24 +137,9 @@ void NativeProxy::installJSIBindings(
return static_cast<double>(output);
};

auto requestRender = [this, getCurrentTime](
auto requestRender = [this](
std::function<void(double)> onRender,
jsi::Runtime &rt) {
// doNoUse -> NodesManager passes here a timestamp from choreographer which
// is useless for us as we use diffrent timer to better handle events. The
// lambda is translated to NodeManager.OnAnimationFrame and treated just
// like reanimated 1 frame callbacks which make use of the timestamp.
auto wrappedOnRender = [getCurrentTime, &rt, onRender](double doNotUse) {
jsi::Object global = rt.global();
jsi::String frameTimestampName =
jsi::String::createFromAscii(rt, "_frameTimestamp");
double frameTimestamp = getCurrentTime();
global.setProperty(rt, frameTimestampName, frameTimestamp);
onRender(frameTimestamp);
global.setProperty(rt, frameTimestampName, jsi::Value::undefined());
};
this->requestRender(std::move(wrappedOnRender));
};
jsi::Runtime &rt) { this->requestRender(onRender); };

#ifdef RCT_NEW_ARCH_ENABLED
auto synchronouslyUpdateUIPropsFunction =
Expand Down
4 changes: 0 additions & 4 deletions ios/native/NativeProxy.mm
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,7 @@ static CFTimeInterval calculateTimestampWithSlowAnimations(CFTimeInterval curren
auto requestRender = [nodesManager, &module](std::function<void(double)> onRender, jsi::Runtime &rt) {
[nodesManager postOnAnimation:^(CADisplayLink *displayLink) {
double frameTimestamp = calculateTimestampWithSlowAnimations(displayLink.targetTimestamp) * 1000;
jsi::Object global = rt.global();
jsi::String frameTimestampName = jsi::String::createFromAscii(rt, "_frameTimestamp");
global.setProperty(rt, frameTimestampName, frameTimestamp);
onRender(frameTimestamp);
global.setProperty(rt, frameTimestampName, jsi::Value::undefined());
}];
};

Expand Down
Loading

0 comments on commit 535b5d6

Please sign in to comment.