diff --git a/.github/workflows/static-root-checks.yml b/.github/workflows/static-root-checks.yml index 32f9daa4bae..a1771eda5e5 100644 --- a/.github/workflows/static-root-checks.yml +++ b/.github/workflows/static-root-checks.yml @@ -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 diff --git a/Common/cpp/AnimatedSensor/AnimatedSensorModule.cpp b/Common/cpp/AnimatedSensor/AnimatedSensorModule.cpp index 4735b085313..f2600661517 100644 --- a/Common/cpp/AnimatedSensor/AnimatedSensorModule.cpp +++ b/Common/cpp/AnimatedSensor/AnimatedSensorModule.cpp @@ -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]); @@ -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) { diff --git a/Common/cpp/NativeModules/NativeReanimatedModule.cpp b/Common/cpp/NativeModules/NativeReanimatedModule.cpp index 05b8512a31f..3b287a4d5b3 100644 --- a/Common/cpp/NativeModules/NativeReanimatedModule.cpp +++ b/Common/cpp/NativeModules/NativeReanimatedModule.cpp @@ -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( - newRegistrationId, eventName, std::move(handlerFunction)); - eventHandlerRegistry->registerEventHandler(handler); + runtimeHelper, + newRegistrationId, + eventName, + std::move(handlerFunction)); + eventHandlerRegistry->registerEventHandler(std::move(handler)); }); return jsi::Value(static_cast(newRegistrationId)); @@ -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( @@ -421,20 +415,10 @@ void NativeReanimatedModule::maybeRequestRender() { } void NativeReanimatedModule::onRender(double timestampMs) { - try { - std::vector 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 callbacks = frameCallbacks; + frameCallbacks.clear(); + for (auto &callback : callbacks) { + callback(timestampMs); } } @@ -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 @@ -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()); } diff --git a/Common/cpp/NativeModules/NativeReanimatedModule.h b/Common/cpp/NativeModules/NativeReanimatedModule.h index e1a426eface..d51a6c771e9 100644 --- a/Common/cpp/NativeModules/NativeReanimatedModule.h +++ b/Common/cpp/NativeModules/NativeReanimatedModule.h @@ -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); diff --git a/Common/cpp/Registries/EventHandlerRegistry.cpp b/Common/cpp/Registries/EventHandlerRegistry.cpp index bd0fd509197..26d316777f7 100644 --- a/Common/cpp/Registries/EventHandlerRegistry.cpp +++ b/Common/cpp/Registries/EventHandlerRegistry.cpp @@ -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> handlersForEvent; @@ -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); } } diff --git a/Common/cpp/Registries/EventHandlerRegistry.h b/Common/cpp/Registries/EventHandlerRegistry.h index 8a2092bd62c..d58a07b47d8 100644 --- a/Common/cpp/Registries/EventHandlerRegistry.h +++ b/Common/cpp/Registries/EventHandlerRegistry.h @@ -29,6 +29,7 @@ class EventHandlerRegistry { void processEvent( jsi::Runtime &rt, + double eventTimestamp, const std::string &eventName, const jsi::Value &eventPayload); diff --git a/Common/cpp/Tools/RuntimeDecorator.cpp b/Common/cpp/Tools/RuntimeDecorator.cpp index 75cc895042b..ae05d1a285c 100644 --- a/Common/cpp/Tools/RuntimeDecorator.cpp +++ b/Common/cpp/Tools/RuntimeDecorator.cpp @@ -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( @@ -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( diff --git a/Common/cpp/Tools/WorkletEventHandler.cpp b/Common/cpp/Tools/WorkletEventHandler.cpp index 4512fe033e3..3000342f3ac 100644 --- a/Common/cpp/Tools/WorkletEventHandler.cpp +++ b/Common/cpp/Tools/WorkletEventHandler.cpp @@ -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 diff --git a/Common/cpp/Tools/WorkletEventHandler.h b/Common/cpp/Tools/WorkletEventHandler.h index b9f0367b98d..63c2b1d9203 100644 --- a/Common/cpp/Tools/WorkletEventHandler.h +++ b/Common/cpp/Tools/WorkletEventHandler.h @@ -1,9 +1,12 @@ #pragma once #include +#include #include #include +#include "Shareables.h" + using namespace facebook; namespace reanimated { @@ -14,17 +17,22 @@ class WorkletEventHandler { friend EventHandlerRegistry; private: + std::shared_ptr _runtimeHelper; uint64_t id; std::string eventName; - jsi::Function handler; + jsi::Value _handlerFunction; public: WorkletEventHandler( + const std::shared_ptr &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 diff --git a/__tests__/Animation.test.js b/__tests__/Animation.test.js index 5859c1c13ef..d9833af1371 100644 --- a/__tests__/Animation.test.js +++ b/__tests__/Animation.test.js @@ -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(); @@ -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); }); @@ -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); }); diff --git a/__tests__/InterpolateColor.test.js b/__tests__/InterpolateColor.test.js index b7d931119df..4210a3290ac 100644 --- a/__tests__/InterpolateColor.test.js +++ b/__tests__/InterpolateColor.test.js @@ -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']; @@ -157,7 +171,6 @@ describe('colors interpolation', () => { fireEvent.press(button); jest.advanceTimersByTime(250); - jest.runOnlyPendingTimers(); expect(view).toHaveAnimatedStyle( { backgroundColor: 'rgba(71, 117, 73, 1)' }, @@ -165,7 +178,6 @@ describe('colors interpolation', () => { ); jest.advanceTimersByTime(250); - jest.runOnlyPendingTimers(); expect(view).toHaveAnimatedStyle( { backgroundColor: 'rgba(96, 144, 32, 1)' }, diff --git a/android/src/main/cpp/NativeProxy.cpp b/android/src/main/cpp/NativeProxy.cpp index 7ae41f83621..257c38600e9 100644 --- a/android/src/main/cpp/NativeProxy.cpp +++ b/android/src/main/cpp/NativeProxy.cpp @@ -137,24 +137,9 @@ void NativeProxy::installJSIBindings( return static_cast(output); }; - auto requestRender = [this, getCurrentTime]( + auto requestRender = [this]( std::function 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 = diff --git a/ios/native/NativeProxy.mm b/ios/native/NativeProxy.mm index 20b2ef45849..cba9b0cade3 100644 --- a/ios/native/NativeProxy.mm +++ b/ios/native/NativeProxy.mm @@ -153,11 +153,7 @@ static CFTimeInterval calculateTimestampWithSlowAnimations(CFTimeInterval curren auto requestRender = [nodesManager, &module](std::function 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()); }]; }; diff --git a/plugin/index.js b/plugin/index.js index 6ef6ac34f5e..504dc7a76f5 100644 --- a/plugin/index.js +++ b/plugin/index.js @@ -33,7 +33,6 @@ const globals = new Set([ 'this', 'console', 'performance', - '_chronoNow', 'Date', 'Array', 'ArrayBuffer', @@ -58,6 +57,7 @@ const globals = new Set([ 'null', 'UIManager', 'requestAnimationFrame', + 'setImmediate', '_WORKLET', 'arguments', 'Boolean', @@ -84,8 +84,6 @@ const globals = new Set([ '_dispatchCommand', '_setGestureState', '_getCurrentTime', - '_eventTimestamp', - '_frameTimestamp', 'isNaN', 'LayoutAnimationRepository', '_notifyAboutProgress', diff --git a/src/reanimated2/NativeReanimated/NativeReanimated.ts b/src/reanimated2/NativeReanimated/NativeReanimated.ts index 7c04dd31dd4..3eb82eb438c 100644 --- a/src/reanimated2/NativeReanimated/NativeReanimated.ts +++ b/src/reanimated2/NativeReanimated/NativeReanimated.ts @@ -24,10 +24,6 @@ export class NativeReanimated { } } - getTimestamp(): number { - throw new Error('stub implementation, used on the web only'); - } - installCoreFunctions( callGuard: , U>( fn: (...args: T) => U, diff --git a/src/reanimated2/core.ts b/src/reanimated2/core.ts index ae42b26ccd0..761bb4e772a 100644 --- a/src/reanimated2/core.ts +++ b/src/reanimated2/core.ts @@ -20,7 +20,6 @@ import { initializeUIRuntime } from './initializers'; export { stopMapper } from './mappers'; export { runOnJS, runOnUI } from './threads'; -export { getTimestamp } from './time'; export type ReanimatedConsole = Pick< Console, @@ -115,9 +114,16 @@ export function registerEventHandler( eventHash: string, eventHandler: (event: T) => void ): string { + function handleAndFlushImmediates(eventTimestamp: number, event: T) { + 'worklet'; + global.__frameTimestamp = eventTimestamp; + eventHandler(event); + global.__flushAnimationFrame(eventTimestamp); + global.__frameTimestamp = undefined; + } return NativeReanimatedModule.registerEventHandler( eventHash, - makeShareableCloneRecursive(eventHandler) + makeShareableCloneRecursive(handleAndFlushImmediates) ); } diff --git a/src/reanimated2/frameCallback/FrameCallbackRegistryUI.ts b/src/reanimated2/frameCallback/FrameCallbackRegistryUI.ts index 4e04cea2f55..4183b2a69d0 100644 --- a/src/reanimated2/frameCallback/FrameCallbackRegistryUI.ts +++ b/src/reanimated2/frameCallback/FrameCallbackRegistryUI.ts @@ -1,4 +1,4 @@ -import { runOnUI } from '../core'; +import { runOnUIImmediately } from '../threads'; type CallbackDetails = { callback: (frameInfo: FrameInfo) => void; @@ -24,7 +24,7 @@ export interface FrameCallbackRegistryUI { manageStateFrameCallback: (callbackId: number, state: boolean) => void; } -export const prepareUIRegistry = runOnUI(() => { +export const prepareUIRegistry = runOnUIImmediately(() => { 'worklet'; const frameCallbackRegistry: FrameCallbackRegistryUI = { diff --git a/src/reanimated2/globals.d.ts b/src/reanimated2/globals.d.ts index 1ff92e9350b..ff208b7834d 100644 --- a/src/reanimated2/globals.d.ts +++ b/src/reanimated2/globals.d.ts @@ -15,8 +15,6 @@ declare global { const _WORKLET: boolean; const _IS_FABRIC: boolean; const _REANIMATED_VERSION_CPP: string; - const _frameTimestamp: number | null; - const _eventTimestamp: number; const __reanimatedModuleProxy: NativeReanimated; const evalWithSourceMap: ( js: string, @@ -26,7 +24,6 @@ declare global { const evalWithSourceUrl: (js: string, sourceURL: string) => any; const _log: (s: string) => void; const _getCurrentTime: () => number; - const _getTimestamp: () => number; const _notifyAboutProgress: ( tag: number, value: number, @@ -66,7 +63,6 @@ declare global { commandName: string, args: Array ) => void; - const _chronoNow: () => number; const performance: { now: () => number }; const ReanimatedDataMock: { now: () => number; @@ -76,6 +72,7 @@ declare global { }; const _frameCallbackRegistry: FrameCallbackRegistryUI; const requestAnimationFrame: (callback: (time: number) => void) => number; + const setImmediate: (callback: (time: number) => void) => number; const console: Console; namespace NodeJS { @@ -83,9 +80,8 @@ declare global { _WORKLET: boolean; _IS_FABRIC: boolean; _REANIMATED_VERSION_CPP: string; - _frameTimestamp: number | null; - _eventTimestamp: number; __reanimatedModuleProxy: NativeReanimated; + __frameTimestamp?: number; evalWithSourceMap: ( js: string, sourceURL: string, @@ -94,7 +90,6 @@ declare global { evalWithSourceUrl: (js: string, sourceURL: string) => any; _log: (s: string) => void; _getCurrentTime: () => number; - _getTimestamp: () => number; _setGestureState: (handlerTag: number, newState: number) => void; _makeShareableClone: (value: any) => any; _updateDataSynchronously: ( @@ -124,7 +119,6 @@ declare global { commandName: string, args: Array ) => void; - _chronoNow: () => number; performance: { now: () => number }; LayoutAnimationsManager: { start: LayoutAnimationStartFunction; @@ -139,7 +133,10 @@ declare global { __workletsCache?: Map any>; __handleCache?: WeakMap; __mapperRegistry?: MapperRegistry; + __flushImmediates: () => void; + __flushAnimationFrame: (frameTimestamp: number) => void; requestAnimationFrame: (callback: (time: number) => void) => number; + setImmediate: (callback: (time: number) => void) => number; console: Console; } } diff --git a/src/reanimated2/hook/useAnimatedStyle.ts b/src/reanimated2/hook/useAnimatedStyle.ts index 59b1c173158..60b5d803ffa 100644 --- a/src/reanimated2/hook/useAnimatedStyle.ts +++ b/src/reanimated2/hook/useAnimatedStyle.ts @@ -1,7 +1,6 @@ -/* global _frameTimestamp */ import { MutableRefObject, useEffect, useRef } from 'react'; -import { startMapper, stopMapper, makeRemote, getTimestamp } from '../core'; +import { startMapper, stopMapper, makeRemote } from '../core'; import updateProps, { updatePropsJestWrapper } from '../UpdateProps'; import { initialUpdaterRun } from '../animation'; import NativeReanimatedModule from '../NativeReanimated'; @@ -55,6 +54,7 @@ interface AnimationRef { } function prepareAnimation( + frameTimestamp: number, animatedProp: AnimatedStyle, lastAnimation: AnimatedStyle, lastValue: AnimatedStyle @@ -63,6 +63,7 @@ function prepareAnimation( if (Array.isArray(animatedProp)) { animatedProp.forEach((prop, index) => { prepareAnimation( + frameTimestamp, prop, lastAnimation && lastAnimation[index], lastValue && lastValue[index] @@ -97,12 +98,13 @@ function prepareAnimation( animation.callStart = (timestamp: Timestamp) => { animation.onStart(animation, value, timestamp, lastAnimation); }; - animation.callStart(getTimestamp()); + animation.callStart(frameTimestamp); animation.callStart = null; } else if (typeof animatedProp === 'object') { // it is an object Object.keys(animatedProp).forEach((key) => prepareAnimation( + frameTimestamp, animatedProp[key], lastAnimation && lastAnimation[key], lastValue && lastValue[key] @@ -186,11 +188,13 @@ function styleUpdater( const nonAnimatedNewValues: StyleProps = {}; let hasAnimations = false; + let frameTimestamp: number | undefined; let hasNonAnimatedValues = false; for (const key in newValues) { const value = newValues[key]; if (isAnimated(value)) { - prepareAnimation(value, animations[key], oldValues[key]); + frameTimestamp = global.__frameTimestamp || performance.now(); + prepareAnimation(frameTimestamp, value, animations[key], oldValues[key]); animations[key] = value; hasAnimations = true; } else { @@ -201,9 +205,8 @@ function styleUpdater( } if (hasAnimations) { - const frame = (_timestamp?: Timestamp) => { + const frame = (timestamp: Timestamp) => { const { animations, last, isAnimationCancelled } = state; - const timestamp = _timestamp ?? getTimestamp(); if (isAnimationCancelled) { state.isAnimationRunning = false; return; @@ -242,11 +245,7 @@ function styleUpdater( if (!state.isAnimationRunning) { state.isAnimationCancelled = false; state.isAnimationRunning = true; - if (_frameTimestamp) { - frame(_frameTimestamp); - } else { - requestAnimationFrame(frame); - } + frame(frameTimestamp!); } if (hasNonAnimatedValues) { @@ -279,6 +278,7 @@ function jestStyleUpdater( // extract animated props let hasAnimations = false; + let frameTimestamp: number | undefined; Object.keys(animations).forEach((key) => { const value = newValues[key]; if (!isAnimated(value)) { @@ -288,15 +288,15 @@ function jestStyleUpdater( Object.keys(newValues).forEach((key) => { const value = newValues[key]; if (isAnimated(value)) { - prepareAnimation(value, animations[key], oldValues[key]); + frameTimestamp = global.__frameTimestamp || performance.now(); + prepareAnimation(frameTimestamp, value, animations[key], oldValues[key]); animations[key] = value; hasAnimations = true; } }); - function frame(_timestamp?: Timestamp) { + function frame(timestamp: Timestamp) { const { animations, last, isAnimationCancelled } = state; - const timestamp = _timestamp ?? getTimestamp(); if (isAnimationCancelled) { state.isAnimationRunning = false; return; @@ -342,11 +342,7 @@ function jestStyleUpdater( if (!state.isAnimationRunning) { state.isAnimationCancelled = false; state.isAnimationRunning = true; - if (_frameTimestamp) { - frame(_frameTimestamp); - } else { - requestAnimationFrame(frame); - } + frame(frameTimestamp!); } } else { state.isAnimationCancelled = true; @@ -410,7 +406,7 @@ export function useAnimatedStyle( if (__DEV__ && !inputs.length && !dependencies && !updater.__workletHash) { throw new Error( `useAnimatedStyle was used without a dependency array or Babel plugin. Please explicitly pass a dependency array, or enable the Babel/SWC plugin. - + For more, see the docs: https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/web-support#web-without-a-babel-plugin` ); } @@ -507,8 +503,6 @@ For more, see the docs: https://docs.swmansion.com/react-native-reanimated/docs/ useEffect(() => { animationsActive.value = true; return () => { - // initRef.current = null; - // viewsRef = null; animationsActive.value = false; }; }, []); diff --git a/src/reanimated2/initializers.ts b/src/reanimated2/initializers.ts index b4aac0caf8a..0f6a641a48a 100644 --- a/src/reanimated2/initializers.ts +++ b/src/reanimated2/initializers.ts @@ -1,7 +1,12 @@ import { reportFatalErrorOnJS } from './errors'; import NativeReanimatedModule from './NativeReanimated'; import { isJest } from './PlatformChecker'; -import { runOnUI, runOnJS } from './threads'; +import { + runOnJS, + setupSetImmediate, + flushImmediates, + runOnUIImmediately, +} from './threads'; // callGuard is only used with debug builds function callGuardDEV, U>( @@ -87,13 +92,63 @@ Possible solutions are: } } +function setupRequestAnimationFrame() { + 'worklet'; + + // Jest mocks requestAnimationFrame API and it does not like if that mock gets overridden + // so we avoid doing requestAnimationFrame batching in Jest environment. + const nativeRequestAnimationFrame = global.requestAnimationFrame; + + let animationFrameCallbacks: Array<(timestamp: number) => void> = []; + + global.__flushAnimationFrame = (frameTimestamp: number) => { + const currentCallbacks = animationFrameCallbacks; + animationFrameCallbacks = []; + currentCallbacks.forEach((f) => f(frameTimestamp)); + flushImmediates(); + }; + + global.requestAnimationFrame = ( + callback: (timestamp: number) => void + ): number => { + animationFrameCallbacks.push(callback); + if (animationFrameCallbacks.length === 1) { + // We schedule native requestAnimationFrame only when the first callback + // is added and then use it to execute all the enqueued callbacks. Once + // the callbacks are run, we clear the array. + nativeRequestAnimationFrame((timestamp) => { + global.__frameTimestamp = timestamp; + global.__flushAnimationFrame(timestamp); + global.__frameTimestamp = undefined; + }); + } + // Reanimated currently does not support cancelling calbacks requested with + // requestAnimationFrame. We return -1 as identifier which isn't in line + // with the spec but it should give users better clue in case they actually + // attempt to store the value returned from rAF and use it for cancelling. + return -1; + }; +} + export function initializeUIRuntime() { NativeReanimatedModule.installCoreFunctions(callGuardDEV, valueUnpacker); const IS_JEST = isJest(); + if (IS_JEST) { + // requestAnimationFrame react-native jest's setup is incorrect as it polyfills + // the method directly using setTimeout, therefore the callback doesn't get the + // expected timestamp as the only argument: https://github.com/facebook/react-native/blob/main/jest/setup.js#L28 + // We override this setup here to make sure that callbacks get the proper timestamps + // when executed. For non-jest environments we define requestAnimationFrame in setupRequestAnimationFrame + // @ts-ignore TypeScript uses Node definition for rAF, setTimeout, etc which returns a Timeout object rather than a number + global.requestAnimationFrame = (callback: (timestamp: number) => void) => { + return setTimeout(() => callback(performance.now()), 0); + }; + } + const capturableConsole = console; - runOnUI(() => { + runOnUIImmediately(() => { 'worklet'; // setup error handler global.ErrorUtils = { @@ -116,30 +171,8 @@ export function initializeUIRuntime() { }; if (!IS_JEST) { - // Jest mocks requestAnimationFrame API and it does not like if that mock gets overridden - // so we avoid doing requestAnimationFrame batching in Jest environment. - const nativeRequestAnimationFrame = global.requestAnimationFrame; - let callbacks: Array<(time: number) => void> = []; - global.requestAnimationFrame = ( - callback: (timestamp: number) => void - ): number => { - callbacks.push(callback); - if (callbacks.length === 1) { - // We schedule native requestAnimationFrame only when the first callback - // is added and then use it to execute all the enqueued callbacks. Once - // the callbacks are run, we clear the array. - nativeRequestAnimationFrame((timestamp) => { - const currentCallbacks = callbacks; - callbacks = []; - currentCallbacks.forEach((f) => f(timestamp)); - }); - } - // Reanimated currently does not support cancelling calbacks requested with - // requestAnimationFrame. We return -1 as identifier which isn't in line - // with the spec but it should give users better clue in case they actually - // attempt to store the value returned from rAF and use it for cancelling. - return -1; - }; + setupSetImmediate(); + setupRequestAnimationFrame(); } })(); } diff --git a/src/reanimated2/js-reanimated/JSReanimated.ts b/src/reanimated2/js-reanimated/JSReanimated.ts index 49093d93891..2fd87ac179d 100644 --- a/src/reanimated2/js-reanimated/JSReanimated.ts +++ b/src/reanimated2/js-reanimated/JSReanimated.ts @@ -5,7 +5,6 @@ import { Value3D, ValueRotation, } from '../commonTypes'; -import { isJest } from '../PlatformChecker'; import { WebSensor } from './WebSensor'; export default class JSReanimated extends NativeReanimated { @@ -14,14 +13,6 @@ export default class JSReanimated extends NativeReanimated { constructor() { super(false); - if (isJest()) { - // on node < 16 jest have problems mocking performance.now method which - // results in the tests failing, since date precision isn't that important - // for tests, we use Date.now instead - this.getTimestamp = () => Date.now(); - } else { - this.getTimestamp = () => window.performance.now(); - } } makeShareableClone(value: T): ShareableRef { diff --git a/src/reanimated2/js-reanimated/commonTypes.ts b/src/reanimated2/js-reanimated/commonTypes.ts index 09d1821bf37..81bdcb05e89 100644 --- a/src/reanimated2/js-reanimated/commonTypes.ts +++ b/src/reanimated2/js-reanimated/commonTypes.ts @@ -32,7 +32,6 @@ export interface JSReanimated { _frames: ((timestamp: Timestamp) => void)[]; timeProvider: { now: () => number }; pushFrame(frame: (timestamp: Timestamp) => void): void; - getTimestamp(): number; maybeRequestRender(): void; _onRender(timestampMs: number): void; installCoreFunctions(valueSetter: (value: T) => void): void; diff --git a/src/reanimated2/js-reanimated/global.ts b/src/reanimated2/js-reanimated/global.ts index 4c1d640fb89..c48b3cf90f6 100644 --- a/src/reanimated2/js-reanimated/global.ts +++ b/src/reanimated2/js-reanimated/global.ts @@ -3,7 +3,6 @@ import { shouldBeUseWeb } from '../PlatformChecker'; const initializeGlobalsForWeb = () => { if (shouldBeUseWeb()) { - global._frameTimestamp = null; global._measure = () => { console.warn( "[Reanimated] You can't use `measure` with Chrome Debugger or with web version" diff --git a/src/reanimated2/js-reanimated/index.ts b/src/reanimated2/js-reanimated/index.ts index 52a9c18adad..a468d9f9b22 100644 --- a/src/reanimated2/js-reanimated/index.ts +++ b/src/reanimated2/js-reanimated/index.ts @@ -5,7 +5,6 @@ const reanimatedJS = new JSReanimated(); global._makeShareableClone = (c) => c; global._scheduleOnJS = setImmediate; -global._getTimestamp = reanimatedJS.getTimestamp.bind(reanimatedJS); interface JSReanimatedComponent { previousStyle: StyleProps; diff --git a/src/reanimated2/layoutReanimation/animationsManager.ts b/src/reanimated2/layoutReanimation/animationsManager.ts index 93db9b64b97..38561a1db57 100644 --- a/src/reanimated2/layoutReanimation/animationsManager.ts +++ b/src/reanimated2/layoutReanimation/animationsManager.ts @@ -1,4 +1,3 @@ -import { runOnUI } from '../core'; import { withStyleAnimation } from '../animation/styleAnimation'; import { SharedValue } from '../commonTypes'; import { makeUIMutable } from '../mutables'; @@ -6,6 +5,7 @@ import { LayoutAnimationFunction, LayoutAnimationsValues, } from './animationBuilder'; +import { runOnUIImmediately } from '../threads'; const TAG_OFFSET = 1e9; @@ -100,7 +100,7 @@ function createLayoutAnimationManager() { }; } -runOnUI(() => { +runOnUIImmediately(() => { 'worklet'; global.LayoutAnimationsManager = createLayoutAnimationManager(); })(); diff --git a/src/reanimated2/mappers.ts b/src/reanimated2/mappers.ts index 4bb300ee3fb..391ee9bc054 100644 --- a/src/reanimated2/mappers.ts +++ b/src/reanimated2/mappers.ts @@ -14,7 +14,7 @@ export function createMapperRegistry() { const mappers = new Map(); let sortedMappers: Mapper[] = []; - let frameRequested = false; + let runRequested = false; function updateMappersOrder() { // sort mappers topologically @@ -74,8 +74,8 @@ export function createMapperRegistry() { sortedMappers = newOrder; } - function mapperFrame() { - frameRequested = false; + function mapperRun() { + runRequested = false; if (mappers.size !== sortedMappers.length) { updateMappersOrder(); } @@ -88,9 +88,9 @@ export function createMapperRegistry() { } function maybeRequestUpdates() { - if (!frameRequested) { - requestAnimationFrame(mapperFrame); - frameRequested = true; + if (!runRequested) { + setImmediate(mapperRun); + runRequested = true; } } diff --git a/src/reanimated2/threads.ts b/src/reanimated2/threads.ts index 5abcbc4c29b..8b46acd18fa 100644 --- a/src/reanimated2/threads.ts +++ b/src/reanimated2/threads.ts @@ -1,11 +1,52 @@ import NativeReanimatedModule from './NativeReanimated'; -import { shouldBeUseWeb } from './PlatformChecker'; +import { isJest, shouldBeUseWeb } from './PlatformChecker'; import { ComplexWorkletFunction } from './commonTypes'; import { makeShareableCloneOnUIRecursive, makeShareableCloneRecursive, } from './shareables'; +const IS_JEST = isJest(); +let _lastSetImmediateFunction: ((callback: () => void) => void) | null = null; + +let _runOnUIQueue: Array<[ComplexWorkletFunction, any[]]> = []; + +export function setupSetImmediate() { + 'worklet'; + + let immediateCalbacks: Array<() => void> = []; + + // @ts-ignore – typescript expects this to conform to NodeJS definition and expects the return value to be NodeJS.Immediate which is an object and not a number + global.setImmediate = (callback: () => void): number => { + immediateCalbacks.push(callback); + return -1; + }; + + global.__flushImmediates = () => { + for (let index = 0; index < immediateCalbacks.length; index += 1) { + // we use classic 'for' loop because the size of the currentTasks array may change while executing some of the callbacks due to setImmediate calls + immediateCalbacks[index](); + } + immediateCalbacks = []; + }; +} + +function flushImmediatesOnUIThread() { + 'worklet'; + global.__flushImmediates(); +} + +export const flushImmediates = shouldBeUseWeb() + ? () => { + // on web flushing is a noop as immediates are handled by the browser + } + : flushImmediatesOnUIThread; + +/** + * Schedule a worklet to execute on the UI runtime. This method does not schedule the work immediately but instead + * waits for other worklets to be scheduled within the same JS loop. It uses setImmediate to schedule all the worklets + * at once making sure they will run within the same frame boundaries on the UI thread. + */ export function runOnUI( worklet: ComplexWorkletFunction ): (...args: A) => void { @@ -14,11 +55,52 @@ export function runOnUI( throw new Error('runOnUI() can only be used on worklets'); } } + return (...args) => { + if (IS_JEST) { + // Jest mocks setImmediate method for each individual tests, because of that + // we may end up not scheduling setImmediate call if the next test starts after + // somce callbacks have been added to a batch from the previous test. To fix this + // we reset the batch queue when setImmediate function changes. + if (_lastSetImmediateFunction !== setImmediate) { + _lastSetImmediateFunction = setImmediate; + _runOnUIQueue = []; + } + } + _runOnUIQueue.push([worklet, args]); + if (_runOnUIQueue.length === 1) { + setImmediate(() => { + const queue = _runOnUIQueue; + _runOnUIQueue = []; + NativeReanimatedModule.scheduleOnUI( + makeShareableCloneRecursive(() => { + 'worklet'; + queue.forEach(([worklet, args]) => { + worklet(...args); + }); + flushImmediates(); + }) + ); + }); + } + }; +} + +/** + * Schedule a worklet to execute on the UI runtime skipping batching mechanism. + */ +export function runOnUIImmediately( + worklet: ComplexWorkletFunction +): (...args: A) => void { + if (__DEV__) { + if (worklet.__workletHash === undefined) { + throw new Error('runOnUI() can only be used on worklets'); + } + } return (...args) => { NativeReanimatedModule.scheduleOnUI( makeShareableCloneRecursive(() => { 'worklet'; - return worklet(...args); + worklet(...args); }) ); }; diff --git a/src/reanimated2/time.ts b/src/reanimated2/time.ts deleted file mode 100644 index 74e347edbe2..00000000000 --- a/src/reanimated2/time.ts +++ /dev/null @@ -1,30 +0,0 @@ -import NativeReanimatedModule from './NativeReanimated'; -import { Platform } from 'react-native'; -import { nativeShouldBeMock } from './PlatformChecker'; -export { stopMapper } from './mappers'; - -let _getTimestamp: () => number; -if (nativeShouldBeMock()) { - _getTimestamp = () => { - return NativeReanimatedModule.getTimestamp(); - }; -} else { - _getTimestamp = () => { - 'worklet'; - if (_frameTimestamp) { - return _frameTimestamp; - } - if (_eventTimestamp) { - return _eventTimestamp; - } - return _getCurrentTime(); - }; -} - -export function getTimestamp(): number { - 'worklet'; - if (Platform.OS === 'web') { - return NativeReanimatedModule.getTimestamp(); - } - return _getTimestamp(); -} diff --git a/src/reanimated2/valueSetter.ts b/src/reanimated2/valueSetter.ts index 88a79fcc37a..5ec73998483 100644 --- a/src/reanimated2/valueSetter.ts +++ b/src/reanimated2/valueSetter.ts @@ -1,6 +1,5 @@ import { AnimationObject, AnimatableValue } from './commonTypes'; import { Descriptor } from './hook/commonTypes'; -import { getTimestamp } from './time'; export { stopMapper } from './mappers'; export function valueSetter(sv: any, value: any): void { @@ -32,7 +31,7 @@ export function valueSetter(sv: any, value: any): void { const initializeAnimation = (timestamp: number) => { animation.onStart(animation, sv.value, timestamp, previousAnimation); }; - const currentTimestamp = getTimestamp(); + const currentTimestamp = global.__frameTimestamp || performance.now(); initializeAnimation(currentTimestamp); const step = (timestamp: number) => { if (animation.cancelled) {