From 09ffd85fc3dd99b4939e6d803a64c0abf0c237c6 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Thu, 15 Dec 2022 12:30:34 +0100 Subject: [PATCH] Route JS errors from UI runtime throught RN's LogBox module (#3846) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR changes the way we report errors in development. Previously we'd use RCTLog native module which would result in a relatively ugly red screen displaying an error. In addition the stack trace wouldn't be symbolicated so it was difficult to reason about the root cause of the problem when the crash happened on the UI runtime. With this change we provide a symbolicated version of trace to ErroUtil module which results in the crash being displayed in the same way as it were to occur on the regular RN runtime. ## Summary The main motivation is to provide better guidance for developers about the crashes on the UI JS runtime as well as to use a familiar UI for displaying those. In a nutshell, the method we use relies on an extended version of "eval" for processing the javascript code when loading on the UI runtime. We now expose `evalWithSourceUrl` that beside the code also allows for assiging the source url that is then included in the traces, and `evalWithSourceMap` (only on hermes) that allows to provide JSON encoded source map that is then used by the javascript engine to symbolicate the stack trace. For JS engines that does not support source maps (JSC), what we do instead, is that we provide the worklet hash as a part of the source url, this allows us to recognize which worklet a given stack entry is coming from and allows us to map the provided line from that worklet into a line of the whole JS bundle. In order to do so, we now generate an "error" object in the place where worklet is generated such that we can get its position in the Javascript bundle. Below is a summary of changes this PR makes: 1) Changes in plugin focus on removing location metadata (we now use a string in a format "worklet_7263" where the number is worklet's hash), adding source maps in a form of JSON encoded string, and adding new error object to the worklet object which is used to remap worklet line number into the bundle line number 2) For the latter, we added some additional logic that replaces entries in the provided error and replaces "worklet_23746:16:2" with the bundle URL along with the remapped line numbers 3) We now route all JS calls via a new method "guardCall" that adds a catch statement and passes the exception back to the main JS runtime where we use ErrorUtils module to trigger the default React Native's LogBox 4) We extend hermes runtime by exposing `evalWithSourceMap` method – this method is only added for debug builds and only on hermes. 5) On other runtimes we register additional global method `evalWIthSourceURL` that makes it possible to provide URLs along the code that needs to be evaluated. ## Test plan Run method "something" from the following snippet and expect an error that should result in a redbox being desplayed. Note that the presented stack trace should have correct line numbers. See the expected result on iOS under the snippet. On Android the errors aren't as nicely formatted but should contain valid trace entries. This needs to be tested on both JSC and Hermes configurations. ``` function makeWorklet() { return () => { 'worklet'; throw new Error('Randomly crashing'); }; } const crashRandomly = makeWorklet(); function anotherWorklet() { 'worklet'; crashRandomly(); } function something() { runOnUI(() => { 'worklet'; anotherWorklet(); })(); } ``` ![simulator_screenshot_3CECE7D6-DA04-4661-93C3-C25EF4A19423](https://user-images.githubusercontent.com/726445/206585057-e3d74c5f-bd02-4e9b-a4ae-ea8cd6fbb709.png) Co-authored-by: Tomek Zawadzki Co-authored-by: Krzysztof Piaskowy Co-authored-by: Juliusz Wajgelt <49338439+jwajgelt@users.noreply.github.com> --- .../NativeModules/NativeReanimatedModule.cpp | 21 ++- .../NativeModules/NativeReanimatedModule.h | 4 +- .../NativeReanimatedModuleSpec.h | 4 +- .../ReanimatedHermesRuntime.cpp | 30 ++++ .../ReanimatedHermesRuntime.h | 2 +- Common/cpp/SharedItems/Shareables.cpp | 4 +- Common/cpp/SharedItems/Shareables.h | 57 +++--- Common/cpp/Tools/RuntimeDecorator.cpp | 31 +++- Common/cpp/Tools/RuntimeDecorator.h | 3 +- __tests__/__snapshots__/plugin.test.js.snap | 165 +++++++++++++----- package.json | 3 +- plugin.js | 82 +++++++-- .../NativeReanimated/NativeReanimated.ts | 13 +- src/reanimated2/core.ts | 61 +------ src/reanimated2/errors.ts | 57 ++++++ src/reanimated2/globals.d.ts | 18 ++ src/reanimated2/initializers.ts | 113 ++++++++++++ src/reanimated2/js-reanimated/JSReanimated.ts | 12 +- src/reanimated2/shareables.ts | 7 + 19 files changed, 525 insertions(+), 162 deletions(-) create mode 100644 src/reanimated2/errors.ts create mode 100644 src/reanimated2/initializers.ts diff --git a/Common/cpp/NativeModules/NativeReanimatedModule.cpp b/Common/cpp/NativeModules/NativeReanimatedModule.cpp index 0aa408ec59f..4330e4fdca4 100644 --- a/Common/cpp/NativeModules/NativeReanimatedModule.cpp +++ b/Common/cpp/NativeModules/NativeReanimatedModule.cpp @@ -58,8 +58,11 @@ NativeReanimatedModule::NativeReanimatedModule( platformDepMethodsHolder.configurePropsFunction) #endif { - auto requestAnimationFrame = [=](FrameCallback callback) { - frameCallbacks.push_back(callback); + auto requestAnimationFrame = [=](jsi::Runtime &rt, const jsi::Value &fn) { + auto jsFunction = std::make_shared(rt, fn); + frameCallbacks.push_back([=](double timestamp) { + runtimeHelper->runOnUIGuarded(*jsFunction, jsi::Value(timestamp)); + }); maybeRequestRender(); }; @@ -174,20 +177,23 @@ NativeReanimatedModule::NativeReanimatedModule( void NativeReanimatedModule::installCoreFunctions( jsi::Runtime &rt, - const jsi::Value &valueUnpacker, - const jsi::Value &layoutAnimationStartFunction) { + const jsi::Value &callGuard, + const jsi::Value &valueUnpacker) { if (!runtimeHelper) { // initialize runtimeHelper here if not already present. We expect only one // instace of the helper to exists. runtimeHelper = std::make_shared(&rt, this->runtime.get(), scheduler); } + runtimeHelper->callGuard = + std::make_unique(runtimeHelper.get(), callGuard); runtimeHelper->valueUnpacker = - std::make_shared(runtimeHelper.get(), valueUnpacker); + std::make_unique(runtimeHelper.get(), valueUnpacker); } NativeReanimatedModule::~NativeReanimatedModule() { if (runtimeHelper) { + runtimeHelper->callGuard = nullptr; runtimeHelper->valueUnpacker = nullptr; // event handler registry and frame callbacks store some JSI values from UI // runtime, so they have to go away before we tear down the runtime @@ -206,11 +212,10 @@ void NativeReanimatedModule::scheduleOnUI( assert( shareableWorklet->valueType() == Shareable::WorkletType && "only worklets can be scheduled to run on UI"); - auto uiRuntime = runtimeHelper->uiRuntime(); frameCallbacks.push_back([=](double timestamp) { - jsi::Runtime &rt = *uiRuntime; + jsi::Runtime &rt = *runtimeHelper->uiRuntime(); auto workletValue = shareableWorklet->getJSValue(rt); - workletValue.asObject(rt).asFunction(rt).call(rt); + runtimeHelper->runOnUIGuarded(workletValue); }); maybeRequestRender(); } diff --git a/Common/cpp/NativeModules/NativeReanimatedModule.h b/Common/cpp/NativeModules/NativeReanimatedModule.h index 1ae695b9c95..d15f196ebbb 100644 --- a/Common/cpp/NativeModules/NativeReanimatedModule.h +++ b/Common/cpp/NativeModules/NativeReanimatedModule.h @@ -49,8 +49,8 @@ class NativeReanimatedModule : public NativeReanimatedModuleSpec, void installCoreFunctions( jsi::Runtime &rt, - const jsi::Value &workletMaker, - const jsi::Value &layoutAnimationStartFunction) override; + const jsi::Value &callGuard, + const jsi::Value &valueUnpacker) override; jsi::Value makeShareableClone(jsi::Runtime &rt, const jsi::Value &value) override; diff --git a/Common/cpp/NativeModules/NativeReanimatedModuleSpec.h b/Common/cpp/NativeModules/NativeReanimatedModuleSpec.h index 3e7393c9022..387004d050c 100644 --- a/Common/cpp/NativeModules/NativeReanimatedModuleSpec.h +++ b/Common/cpp/NativeModules/NativeReanimatedModuleSpec.h @@ -24,8 +24,8 @@ class JSI_EXPORT NativeReanimatedModuleSpec : public TurboModule { public: virtual void installCoreFunctions( jsi::Runtime &rt, - const jsi::Value &valueUnpacker, - const jsi::Value &layoutAnimationStartFunction) = 0; + const jsi::Value &callGuard, + const jsi::Value &valueUnpacker) = 0; // SharedValue virtual jsi::Value makeShareableClone( diff --git a/Common/cpp/ReanimatedRuntime/ReanimatedHermesRuntime.cpp b/Common/cpp/ReanimatedRuntime/ReanimatedHermesRuntime.cpp index bc9f1e5795c..0c772b5364b 100644 --- a/Common/cpp/ReanimatedRuntime/ReanimatedHermesRuntime.cpp +++ b/Common/cpp/ReanimatedRuntime/ReanimatedHermesRuntime.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #if __has_include() @@ -79,6 +80,35 @@ ReanimatedHermesRuntime::ReanimatedHermesRuntime( // that the thread was indeed `quit` before jsQueue->quitSynchronous(); #endif + +#ifdef DEBUG + facebook::hermes::HermesRuntime *wrappedRuntime = runtime_.get(); + jsi::Value evalWithSourceMap = jsi::Function::createFromHostFunction( + *runtime_, + jsi::PropNameID::forAscii(*runtime_, "evalWithSourceMap"), + 3, + [wrappedRuntime]( + jsi::Runtime &rt, + const jsi::Value &thisValue, + const jsi::Value *args, + size_t count) -> jsi::Value { + auto code = std::make_shared( + args[0].asString(rt).utf8(rt)); + std::string sourceURL; + if (count > 1 && args[1].isString()) { + sourceURL = args[1].asString(rt).utf8(rt); + } + std::shared_ptr sourceMap; + if (count > 2 && args[2].isString()) { + sourceMap = std::make_shared( + args[2].asString(rt).utf8(rt)); + } + return wrappedRuntime->evaluateJavaScriptWithSourceMap( + code, sourceMap, sourceURL); + }); + runtime_->global().setProperty( + *runtime_, "evalWithSourceMap", evalWithSourceMap); +#endif // DEBUG } ReanimatedHermesRuntime::~ReanimatedHermesRuntime() { diff --git a/Common/cpp/ReanimatedRuntime/ReanimatedHermesRuntime.h b/Common/cpp/ReanimatedRuntime/ReanimatedHermesRuntime.h index 526cc1ab690..aac9dec1640 100644 --- a/Common/cpp/ReanimatedRuntime/ReanimatedHermesRuntime.h +++ b/Common/cpp/ReanimatedRuntime/ReanimatedHermesRuntime.h @@ -116,7 +116,7 @@ class ReanimatedHermesRuntime ~ReanimatedHermesRuntime(); private: - std::shared_ptr runtime_; + std::unique_ptr runtime_; ReanimatedReentrancyCheck reentrancyCheck_; }; diff --git a/Common/cpp/SharedItems/Shareables.cpp b/Common/cpp/SharedItems/Shareables.cpp index 1cb4bbc3b9e..8708b0405a1 100644 --- a/Common/cpp/SharedItems/Shareables.cpp +++ b/Common/cpp/SharedItems/Shareables.cpp @@ -13,7 +13,9 @@ CoreFunction::CoreFunction( rnFunction_ = std::make_unique(workletObject.asFunction(rt)); functionBody_ = workletObject.getProperty(rt, "asString").asString(rt).utf8(rt); - location_ = workletObject.getProperty(rt, "__location").asString(rt).utf8(rt); + location_ = "worklet_" + + std::to_string(static_cast( + workletObject.getProperty(rt, "__workletHash").getNumber())); } std::unique_ptr &CoreFunction::getFunction(jsi::Runtime &rt) { diff --git a/Common/cpp/SharedItems/Shareables.h b/Common/cpp/SharedItems/Shareables.h index 7a26f815a82..748414100c0 100644 --- a/Common/cpp/SharedItems/Shareables.h +++ b/Common/cpp/SharedItems/Shareables.h @@ -20,7 +20,29 @@ using namespace facebook; namespace reanimated { -class CoreFunction; +class JSRuntimeHelper; + +// Core functions are not allowed to capture outside variables, otherwise they'd +// try to access _closure variable which is something we want to avoid for +// simplicity reasons. +class CoreFunction { + private: + std::unique_ptr rnFunction_; + std::unique_ptr uiFunction_; + std::string functionBody_; + std::string location_; + JSRuntimeHelper + *runtimeHelper_; // runtime helper holds core function references, so we + // use normal pointer here to avoid ref cycles. + std::unique_ptr &getFunction(jsi::Runtime &rt); + + public: + CoreFunction(JSRuntimeHelper *runtimeHelper, const jsi::Value &workletObject); + template + jsi::Value call(jsi::Runtime &rt, Args &&...args) { + return getFunction(rt)->call(rt, args...); + } +}; class JSRuntimeHelper { private: @@ -36,7 +58,8 @@ class JSRuntimeHelper { : rnRuntime_(rnRuntime), uiRuntime_(uiRuntime), scheduler_(scheduler) {} volatile bool uiRuntimeDestroyed; - std::shared_ptr valueUnpacker; + std::unique_ptr callGuard; + std::unique_ptr valueUnpacker; inline jsi::Runtime *uiRuntime() const { return uiRuntime_; @@ -61,27 +84,19 @@ class JSRuntimeHelper { void scheduleOnJS(std::function job) { scheduler_->scheduleOnJS(job); } -}; - -// Core functions are not allowed to capture outside variables, otherwise they'd -// try to access _closure variable which is something we want to avoid for -// simplicity reasons. -class CoreFunction { - private: - std::unique_ptr rnFunction_; - std::unique_ptr uiFunction_; - std::string functionBody_; - std::string location_; - JSRuntimeHelper - *runtimeHelper_; // runtime helper holds core function references, so we - // use normal pointer here to avoid ref cycles. - std::unique_ptr &getFunction(jsi::Runtime &rt); - public: - CoreFunction(JSRuntimeHelper *runtimeHelper, const jsi::Value &workletObject); template - jsi::Value call(jsi::Runtime &rt, Args &&...args) { - return getFunction(rt)->call(rt, args...); + inline void runOnUIGuarded(const jsi::Value &function, Args &&...args) { + // We only use callGuard in debug mode, otherwise we call the provided + // function directly. CallGuard provides a way of capturing exceptions in + // JavaScript and propagating them to the main React Native thread such that + // they can be presented using RN's LogBox. + jsi::Runtime &rt = *uiRuntime_; +#ifdef DEBUG + callGuard->call(rt, function, args...); +#else + function.asObject(rt).asFunction(rt).call(rt, args...); +#endif } }; diff --git a/Common/cpp/Tools/RuntimeDecorator.cpp b/Common/cpp/Tools/RuntimeDecorator.cpp index bf316351980..daa3adc449b 100644 --- a/Common/cpp/Tools/RuntimeDecorator.cpp +++ b/Common/cpp/Tools/RuntimeDecorator.cpp @@ -32,7 +32,30 @@ void RuntimeDecorator::decorateRuntime( rt.global().setProperty(rt, "global", rt.global()); - rt.global().setProperty(rt, "jsThis", jsi::Value::undefined()); +#ifdef DEBUG + auto evalWithSourceUrl = [](jsi::Runtime &rt, + const jsi::Value &thisValue, + const jsi::Value *args, + size_t count) -> jsi::Value { + auto code = std::make_shared( + args[0].asString(rt).utf8(rt)); + std::string url; + if (count > 1 && args[1].isString()) { + url = args[1].asString(rt).utf8(rt); + } + + return rt.evaluateJavaScript(code, url); + }; + + rt.global().setProperty( + rt, + "evalWithSourceUrl", + jsi::Function::createFromHostFunction( + rt, + jsi::PropNameID::forAscii(rt, "evalWithSourceUrl"), + 1, + evalWithSourceUrl)); +#endif // DEBUG auto callback = [](jsi::Runtime &rt, const jsi::Value &thisValue, @@ -208,11 +231,7 @@ void RuntimeDecorator::decorateUIRuntime( const jsi::Value &thisValue, const jsi::Value *args, const size_t count) -> jsi::Value { - auto fun = - std::make_shared(args[0].asObject(rt).asFunction(rt)); - requestFrame([&rt, fun](double timestampMs) { - fun->call(rt, jsi::Value(timestampMs)); - }); + requestFrame(rt, std::move(args[0])); return jsi::Value::undefined(); }; jsi::Value requestAnimationFrame = jsi::Function::createFromHostFunction( diff --git a/Common/cpp/Tools/RuntimeDecorator.h b/Common/cpp/Tools/RuntimeDecorator.h index 5b0f9b2ac2a..f1d8cddb2c1 100644 --- a/Common/cpp/Tools/RuntimeDecorator.h +++ b/Common/cpp/Tools/RuntimeDecorator.h @@ -11,7 +11,8 @@ using namespace facebook; namespace reanimated { -using RequestFrameFunction = std::function)>; +using RequestFrameFunction = + std::function; using ScheduleOnJSFunction = std::function; using MakeShareableCloneFunction = diff --git a/__tests__/__snapshots__/plugin.test.js.snap b/__tests__/__snapshots__/plugin.test.js.snap index 4e5e827a76e..babaa71a130 100644 --- a/__tests__/__snapshots__/plugin.test.js.snap +++ b/__tests__/__snapshots__/plugin.test.js.snap @@ -7,6 +7,8 @@ var objX = { }; var f = function () { + var _e = [new Error(), -3, -20]; + var _f = function _f() { return { res: x + objX.x @@ -21,13 +23,15 @@ var f = function () { }; _f.asString = \\"function f(){const{x,objX}=this._closure;return{res:x+objX.x};}\\"; _f.__workletHash = 5359970077727; - _f.__location = \\"${ process.cwd() }/jest tests fixture (6:6)\\"; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }();" `; - exports[`babel plugin doesn't capture globals 1`] = ` "var f = function () { + var _e = [new Error(), 1, -20]; + var _f = function _f() { console.log('test'); }; @@ -35,7 +39,8 @@ exports[`babel plugin doesn't capture globals 1`] = ` _f._closure = {}; _f.asString = \\"function f(){console.log('test');}\\"; _f.__workletHash = 13298016111221; - _f.__location = \\"${ process.cwd() }/jest tests fixture (2:6)\\"; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }();" `; @@ -48,6 +53,8 @@ exports[`babel plugin doesn't transform standard callback functions 1`] = ` exports[`babel plugin doesn't transform string literals 1`] = ` "var foo = function () { + var _e = [new Error(), 1, -20]; + var _f = function _f(x) { var bar = 'worklet'; var baz = \\"worklet\\"; @@ -56,7 +63,8 @@ exports[`babel plugin doesn't transform string literals 1`] = ` _f._closure = {}; _f.asString = \\"function foo(x){const bar='worklet';const baz=\\\\\\"worklet\\\\\\";}\\"; _f.__workletHash = 9810417751380; - _f.__location = \\"${ process.cwd() }/jest tests fixture (2:6)\\"; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }();" `; @@ -65,6 +73,8 @@ exports[`babel plugin supports recursive calls 1`] = ` "var a = 1; var foo = function () { + var _e = [new Error(), -2, -20]; + var _f = function _f(t) { if (t > 0) { return a + foo(t - 1); @@ -76,7 +86,8 @@ var foo = function () { }; _f.asString = \\"function foo(t){const foo=this._recur;const{a}=this._closure;if(t>0){return a+foo(t-1);}}\\"; _f.__workletHash = 2022702330805; - _f.__location = \\"${ process.cwd() }/jest tests fixture (3:6)\\"; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }();" `; @@ -91,6 +102,8 @@ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && function Box() { var offset = (0, _reactNativeReanimated.useSharedValue)(0); var animatedStyles = (0, _reactNativeReanimated.useAnimatedStyle)(function () { + var _e = [new Error(), -2, -20]; + var _f = function _f() { return { transform: [{ @@ -102,9 +115,10 @@ function Box() { _f._closure = { offset: offset }; - _f.asString = \\"function _f(){const{offset}=this._closure;return{transform:[{translateX:offset.value*255}]};}\\"; - _f.__workletHash = 361788175040; - _f.__location = \\"${ process.cwd() }/jest tests fixture (10:48)\\"; + _f.asString = \\"function anonymous(){const{offset}=this._closure;return{transform:[{translateX:offset.value*255}]};}\\"; + _f.__workletHash = 16669311443114; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; _f.__optimalization = 3; return _f; }()); @@ -121,6 +135,8 @@ function Box() { exports[`babel plugin transforms spread operator in worklets for arrays 1`] = ` "var foo = function () { + var _e = [new Error(), 1, -20]; + var _f = function _f() { var bar = [4, 5]; var baz = [1].concat([2, 3], bar); @@ -129,13 +145,16 @@ exports[`babel plugin transforms spread operator in worklets for arrays 1`] = ` _f._closure = {}; _f.asString = \\"function foo(){const bar=[4,5];const baz=[1,...[2,3],...bar];}\\"; _f.__workletHash = 3161057533258; - _f.__location = \\"${ process.cwd() }/jest tests fixture (2:6)\\"; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }();" `; exports[`babel plugin transforms spread operator in worklets for function arguments 1`] = ` "var foo = function () { + var _e = [new Error(), 1, -20]; + var _f = function _f() { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; @@ -147,7 +166,8 @@ exports[`babel plugin transforms spread operator in worklets for function argume _f._closure = {}; _f.asString = \\"function foo(...args){console.log(args);}\\"; _f.__workletHash = 9866931756941; - _f.__location = \\"${ process.cwd() }/jest tests fixture (2:6)\\"; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }();" `; @@ -158,6 +178,8 @@ exports[`babel plugin transforms spread operator in worklets for function calls var _toConsumableArray2 = _interopRequireDefault(require(\\"@babel/runtime/helpers/toConsumableArray\\")); var foo = function () { + var _e = [new Error(), 1, -20]; + var _f = function _f(arg) { var _console; @@ -167,13 +189,16 @@ var foo = function () { _f._closure = {}; _f.asString = \\"function foo(arg){console.log(...arg);}\\"; _f.__workletHash = 2015887751437; - _f.__location = \\"${ process.cwd() }/jest tests fixture (2:6)\\"; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }();" `; exports[`babel plugin transforms spread operator in worklets for objects 1`] = ` "var foo = function () { + var _e = [new Error(), 1, -20]; + var _f = function _f() { var bar = { d: 4, @@ -190,27 +215,33 @@ exports[`babel plugin transforms spread operator in worklets for objects 1`] = ` _f._closure = {}; _f.asString = \\"function foo(){const bar={d:4,e:5};const baz={a:1,...{b:2,c:3},...bar};}\\"; _f.__workletHash = 792186851025; - _f.__location = \\"${ process.cwd() }/jest tests fixture (2:6)\\"; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }();" `; exports[`babel plugin workletizes ArrowFunctionExpression 1`] = ` "var foo = function () { + var _e = [new Error(), 1, -20]; + var _f = function _f(x) { return x + 2; }; _f._closure = {}; - _f.asString = \\"function _f(x){return x+2;}\\"; - _f.__workletHash = 11411090164019; - _f.__location = \\"${ process.cwd() }/jest tests fixture (2:18)\\"; + _f.asString = \\"function anonymous(x){return x+2;}\\"; + _f.__workletHash = 16347365292089; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }();" `; exports[`babel plugin workletizes FunctionDeclaration 1`] = ` "var foo = function () { + var _e = [new Error(), 1, -20]; + var _f = function _f(x) { return x + 2; }; @@ -218,7 +249,8 @@ exports[`babel plugin workletizes FunctionDeclaration 1`] = ` _f._closure = {}; _f.asString = \\"function foo(x){return x+2;}\\"; _f.__workletHash = 4679479961836; - _f.__location = \\"${ process.cwd() }/jest tests fixture (2:6)\\"; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }();" `; @@ -238,6 +270,8 @@ var Foo = function () { (0, _createClass2.default)(Foo, [{ key: \\"bar\\", get: function () { + var _e = [new Error(), -2, -20]; + var _f = function _f() { return x + 2; }; @@ -248,6 +282,7 @@ var Foo = function () { _f.asString = \\"function get(){const{x}=this._closure;return x+2;}\\"; _f.__workletHash = 10436985806815; _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }() }]); @@ -257,6 +292,8 @@ var Foo = function () { exports[`babel plugin workletizes hook wrapped ArrowFunctionExpression automatically 1`] = ` "var animatedStyle = useAnimatedStyle(function () { + var _e = [new Error(), 1, -20]; + var _f = function _f() { return { width: 50 @@ -264,9 +301,10 @@ exports[`babel plugin workletizes hook wrapped ArrowFunctionExpression automatic }; _f._closure = {}; - _f.asString = \\"function _f(){return{width:50};}\\"; - _f.__workletHash = 9756190407413; - _f.__location = \\"${ process.cwd() }/jest tests fixture (2:45)\\"; + _f.asString = \\"function anonymous(){return{width:50};}\\"; + _f.__workletHash = 9645206935615; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; _f.__optimalization = 3; return _f; }());" @@ -274,6 +312,8 @@ exports[`babel plugin workletizes hook wrapped ArrowFunctionExpression automatic exports[`babel plugin workletizes hook wrapped named FunctionExpression automatically 1`] = ` "var animatedStyle = useAnimatedStyle(function () { + var _e = [new Error(), 1, -20]; + var _f = function _f() { return { width: 50 @@ -283,7 +323,8 @@ exports[`babel plugin workletizes hook wrapped named FunctionExpression automati _f._closure = {}; _f.asString = \\"function foo(){return{width:50};}\\"; _f.__workletHash = 6275510763626; - _f.__location = \\"${ process.cwd() }/jest tests fixture (2:45)\\"; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; _f.__optimalization = 3; return _f; }());" @@ -291,6 +332,8 @@ exports[`babel plugin workletizes hook wrapped named FunctionExpression automati exports[`babel plugin workletizes hook wrapped unnamed FunctionExpression automatically 1`] = ` "var animatedStyle = useAnimatedStyle(function () { + var _e = [new Error(), 1, -20]; + var _f = function _f() { return { width: 50 @@ -298,9 +341,10 @@ exports[`babel plugin workletizes hook wrapped unnamed FunctionExpression automa }; _f._closure = {}; - _f.asString = \\"function _f(){return{width:50};}\\"; - _f.__workletHash = 9756190407413; - _f.__location = \\"${ process.cwd() }/jest tests fixture (2:45)\\"; + _f.asString = \\"function anonymous(){return{width:50};}\\"; + _f.__workletHash = 9645206935615; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; _f.__optimalization = 3; return _f; }());" @@ -321,6 +365,8 @@ var Foo = function () { (0, _createClass2.default)(Foo, [{ key: \\"bar\\", value: function () { + var _e = [new Error(), 1, -20]; + var _f = function _f(x) { return x + 2; }; @@ -329,6 +375,7 @@ var Foo = function () { _f.asString = \\"function bar(x){return x+2;}\\"; _f.__workletHash = 16974800582491; _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }() }]); @@ -338,6 +385,8 @@ var Foo = function () { exports[`babel plugin workletizes named FunctionExpression 1`] = ` "var foo = function () { + var _e = [new Error(), 1, -20]; + var _f = function _f(x) { return x + 2; }; @@ -345,7 +394,8 @@ exports[`babel plugin workletizes named FunctionExpression 1`] = ` _f._closure = {}; _f.asString = \\"function foo(x){return x+2;}\\"; _f.__workletHash = 4679479961836; - _f.__location = \\"${ process.cwd() }/jest tests fixture (2:18)\\"; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }();" `; @@ -353,14 +403,17 @@ exports[`babel plugin workletizes named FunctionExpression 1`] = ` exports[`babel plugin workletizes object hook wrapped ArrowFunctionExpression automatically 1`] = ` "useAnimatedGestureHandler({ onStart: function () { + var _e = [new Error(), 1, -20]; + var _f = function _f(event) { console.log(event); }; _f._closure = {}; - _f.asString = \\"function _f(event){console.log(event);}\\"; - _f.__workletHash = 2164830539996; - _f.__location = \\"${ process.cwd() }/jest tests fixture (3:17)\\"; + _f.asString = \\"function anonymous(event){console.log(event);}\\"; + _f.__workletHash = 1022605193782; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }() });" @@ -369,6 +422,8 @@ exports[`babel plugin workletizes object hook wrapped ArrowFunctionExpression au exports[`babel plugin workletizes object hook wrapped ObjectMethod automatically 1`] = ` "useAnimatedGestureHandler({ onStart: function () { + var _e = [new Error(), 1, -20]; + var _f = function _f(event) { console.log(event); }; @@ -376,7 +431,8 @@ exports[`babel plugin workletizes object hook wrapped ObjectMethod automatically _f._closure = {}; _f.asString = \\"function onStart(event){console.log(event);}\\"; _f.__workletHash = 338158776260; - _f.__location = \\"${ process.cwd() }/jest tests fixture (3:8)\\"; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }() });" @@ -385,6 +441,8 @@ exports[`babel plugin workletizes object hook wrapped ObjectMethod automatically exports[`babel plugin workletizes object hook wrapped named FunctionExpression automatically 1`] = ` "useAnimatedGestureHandler({ onStart: function () { + var _e = [new Error(), 1, -20]; + var _f = function _f(event) { console.log(event); }; @@ -392,7 +450,8 @@ exports[`babel plugin workletizes object hook wrapped named FunctionExpression a _f._closure = {}; _f.asString = \\"function onStart(event){console.log(event);}\\"; _f.__workletHash = 338158776260; - _f.__location = \\"${ process.cwd() }/jest tests fixture (3:17)\\"; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }() });" @@ -401,14 +460,17 @@ exports[`babel plugin workletizes object hook wrapped named FunctionExpression a exports[`babel plugin workletizes object hook wrapped unnamed FunctionExpression automatically 1`] = ` "useAnimatedGestureHandler({ onStart: function () { + var _e = [new Error(), 1, -20]; + var _f = function _f(event) { console.log(event); }; _f._closure = {}; - _f.asString = \\"function _f(event){console.log(event);}\\"; - _f.__workletHash = 2164830539996; - _f.__location = \\"${ process.cwd() }/jest tests fixture (3:17)\\"; + _f.asString = \\"function anonymous(event){console.log(event);}\\"; + _f.__workletHash = 1022605193782; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }() });" @@ -418,34 +480,43 @@ exports[`babel plugin workletizes possibly chained gesture object callback funct "var _reactNativeGestureHandler = require(\\"react-native-gesture-handler\\"); var foo = _reactNativeGestureHandler.Gesture.Tap().numberOfTaps(2).onBegin(function () { + var _e = [new Error(), 1, -20]; + var _f = function _f() { console.log('onBegin'); }; _f._closure = {}; - _f.asString = \\"function _f(){console.log('onBegin');}\\"; - _f.__workletHash = 13662490049850; - _f.__location = \\"${ process.cwd() }/jest tests fixture (6:17)\\"; + _f.asString = \\"function anonymous(){console.log('onBegin');}\\"; + _f.__workletHash = 15393478329680; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }()).onStart(function () { + var _e = [new Error(), 1, -20]; + var _f = function _f(_event) { console.log('onStart'); }; _f._closure = {}; - _f.asString = \\"function _f(_event){console.log('onStart');}\\"; - _f.__workletHash = 16334902412526; - _f.__location = \\"${ process.cwd() }/jest tests fixture (9:17)\\"; + _f.asString = \\"function anonymous(_event){console.log('onStart');}\\"; + _f.__workletHash = 12748187344900; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }()).onEnd(function () { + var _e = [new Error(), 1, -20]; + var _f = function _f(_event, _success) { console.log('onEnd'); }; _f._closure = {}; - _f.asString = \\"function _f(_event,_success){console.log('onEnd');}\\"; - _f.__workletHash = 4053780716017; - _f.__location = \\"${ process.cwd() }/jest tests fixture (12:15)\\"; + _f.asString = \\"function anonymous(_event,_success){console.log('onEnd');}\\"; + _f.__workletHash = 232586479291; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }());" `; @@ -465,6 +536,8 @@ var Foo = function () { (0, _createClass2.default)(Foo, null, [{ key: \\"bar\\", value: function () { + var _e = [new Error(), 1, -20]; + var _f = function _f(x) { return x + 2; }; @@ -473,6 +546,7 @@ var Foo = function () { _f.asString = \\"function bar(x){return x+2;}\\"; _f.__workletHash = 16974800582491; _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }() }]); @@ -482,14 +556,17 @@ var Foo = function () { exports[`babel plugin workletizes unnamed FunctionExpression 1`] = ` "var foo = function () { + var _e = [new Error(), 1, -20]; + var _f = function _f(x) { return x + 2; }; _f._closure = {}; - _f.asString = \\"function _f(x){return x+2;}\\"; - _f.__workletHash = 11411090164019; - _f.__location = \\"${ process.cwd() }/jest tests fixture (2:18)\\"; + _f.asString = \\"function anonymous(x){return x+2;}\\"; + _f.__workletHash = 16347365292089; + _f.__location = \\"${ process.cwd() }/jest tests fixture\\"; + _f.__stackDetails = _e; return _f; }();" `; diff --git a/package.json b/package.json index fc6dd8ef2f9..77a3b4b5077 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,8 @@ "invariant": "^2.2.4", "lodash.isequal": "^4.5.0", "setimmediate": "^1.0.5", - "string-hash-64": "^1.0.3" + "string-hash-64": "^1.0.3", + "convert-source-map": "^1.7.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0", diff --git a/plugin.js b/plugin.js index 5c8d2e438d9..73bbda0122b 100644 --- a/plugin.js +++ b/plugin.js @@ -4,6 +4,7 @@ const hash = require('string-hash-64'); const traverse = require('@babel/traverse').default; const { transformSync } = require('@babel/core'); const fs = require('fs'); +const convertSourceMap = require('convert-source-map'); /** * holds a map of function names as keys and array of argument indexes as values which should be automatically workletized(they have to be functions)(starting from 0) */ @@ -76,6 +77,7 @@ const globals = new Set([ '_removeShadowNodeFromRegistry', 'RegExp', 'Error', + 'ErrorUtils', 'global', '_measure', '_scrollTo', @@ -380,10 +382,12 @@ function buildWorkletString(t, fun, closureVariables, name, inputMap) { } } + const includeSourceMap = shouldGenerateSourceMap(); + const transformed = transformSync(code, { plugins: [prependClosureVariablesIfNecessary()], - compact: !shouldGenerateSourceMap(), - sourceMaps: shouldGenerateSourceMap() ? 'inline' : false, + compact: !includeSourceMap, + sourceMaps: includeSourceMap, inputSourceMap: inputMap, ast: false, babelrc: false, @@ -391,7 +395,17 @@ function buildWorkletString(t, fun, closureVariables, name, inputMap) { comments: false, }); - return transformed.code; + let sourceMap; + if (includeSourceMap) { + sourceMap = convertSourceMap.fromObject(transformed.map).toObject(); + // sourcesContent field contains a full source code of the file which contains the worklet + // and is not needed by the source map interpreter in order to symbolicate a stack trace. + // Therefore, we remove it to reduce the bandwith and avoid sending it potentially multiple times + // in files that contain multiple worklets. Along with sourcesContent. + delete sourceMap.sourcesContent; + } + + return [transformed.code, JSON.stringify(sourceMap)]; } function makeWorkletName(t, fun) { @@ -404,7 +418,7 @@ function makeWorkletName(t, fun) { if (t.isFunctionExpression(fun) && t.isIdentifier(fun.node.id)) { return fun.node.id.name; } - return '_f'; // fallback for ArrowFunctionExpression and unnamed FunctionExpression + return 'anonymous'; // fallback for ArrowFunctionExpression and unnamed FunctionExpression } function makeWorklet(t, fun, state) { @@ -513,7 +527,7 @@ function makeWorklet(t, fun, state) { funExpression = clone; } - const funString = buildWorkletString( + const [funString, sourceMapString] = buildWorkletString( t, transformed.ast, variables, @@ -528,12 +542,14 @@ function makeWorklet(t, fun, state) { location = path.relative(state.cwd, location); } - const loc = fun && fun.node && fun.node.loc && fun.node.loc.start; - if (loc) { - const { line, column } = loc; - if (typeof line === 'number' && typeof column === 'number') { - location = `${location} (${line}:${column})`; - } + let lineOffset = 1; + if (closure.size > 0) { + // When worklet captures some variables, we append closure destructing at + // the beginning of the function body. This effectively results in line + // numbers shifting by the number of captured variables (size of the + // closure) + 2 (for the opening and closing brackets of the destruct + // statement) + lineOffset -= closure.size + 2; } const statements = [ @@ -578,6 +594,50 @@ function makeWorklet(t, fun, state) { ), ]; + if (sourceMapString) { + statements.push( + t.expressionStatement( + t.assignmentExpression( + '=', + t.memberExpression( + privateFunctionId, + t.identifier('__sourceMap'), + false + ), + t.stringLiteral(sourceMapString) + ) + ) + ); + } + + if (!isRelease()) { + statements.unshift( + t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier('_e'), + t.arrayExpression([ + t.newExpression(t.identifier('Error'), []), + t.numericLiteral(lineOffset), + t.numericLiteral(-20), // the placement of opening bracket after Exception in line that defined '_e' variable + ]) + ), + ]) + ); + statements.push( + t.expressionStatement( + t.assignmentExpression( + '=', + t.memberExpression( + privateFunctionId, + t.identifier('__stackDetails'), + false + ), + t.identifier('_e') + ) + ) + ); + } + if (options && options.optFlags) { statements.push( t.expressionStatement( diff --git a/src/reanimated2/NativeReanimated/NativeReanimated.ts b/src/reanimated2/NativeReanimated/NativeReanimated.ts index 660fa6cc90f..04952384960 100644 --- a/src/reanimated2/NativeReanimated/NativeReanimated.ts +++ b/src/reanimated2/NativeReanimated/NativeReanimated.ts @@ -47,8 +47,17 @@ export class NativeReanimated { throw new Error('stub implementation, used on the web only'); } - installCoreFunctions(valueUnpacker: (value: T) => T): void { - return this.InnerNativeModule.installCoreFunctions(valueUnpacker); + installCoreFunctions( + callGuard: , U>( + fn: (...args: T) => U, + ...args: T + ) => void, + valueUnpacker: (value: T) => T + ): void { + return this.InnerNativeModule.installCoreFunctions( + callGuard, + valueUnpacker + ); } makeShareableClone(value: T): ShareableRef { diff --git a/src/reanimated2/core.ts b/src/reanimated2/core.ts index 8a2ea15a941..f1a19e08cee 100644 --- a/src/reanimated2/core.ts +++ b/src/reanimated2/core.ts @@ -5,13 +5,13 @@ import { makeShareableCloneRecursive, makeShareable as makeShareableUnwrapped, } from './shareables'; -import { runOnUI, runOnJS } from './threads'; import { startMapper as startMapperUnwrapped } from './mappers'; import { makeMutable as makeMutableUnwrapped, makeRemote as makeRemoteUnwrapped, } from './mutables'; import { LayoutAnimationFunction } from './layoutReanimation'; +import { initializeUIRuntime } from './initializers'; export { stopMapper } from './mappers'; export { runOnJS, runOnUI } from './threads'; @@ -106,49 +106,6 @@ export function getViewProp(viewTag: string, propName: string): Promise { }); } -function valueUnpacker(objectToUnpack: any, category?: string): any { - 'worklet'; - let workletsCache = global.__workletsCache; - let handleCache = global.__handleCache; - if (workletsCache === undefined) { - // init - workletsCache = global.__workletsCache = new Map(); - handleCache = global.__handleCache = new WeakMap(); - } - if (objectToUnpack.__workletHash) { - let workletFun = workletsCache.get(objectToUnpack.__workletHash); - if (workletFun === undefined) { - // eslint-disable-next-line no-eval - workletFun = eval('(' + objectToUnpack.asString + '\n)') as ( - ...args: any[] - ) => any; - workletsCache.set(objectToUnpack.__workletHash, workletFun); - } - const functionInstance = workletFun.bind(objectToUnpack); - objectToUnpack._recur = functionInstance; - return functionInstance; - } else if (objectToUnpack.__init) { - let value = handleCache!.get(objectToUnpack); - if (value === undefined) { - value = objectToUnpack.__init(); - handleCache!.set(objectToUnpack, value); - } - return value; - } else if (category === 'RemoteFunction') { - const fun = () => { - throw new Error(`Tried to synchronously call a non-worklet function on the UI thread. - -Possible solutions are: - a) If you want to synchronously execute this method, mark it as a worklet - b) If you want to execute this function on the JS thread, wrap it using \`runOnJS\``); - }; - fun.__remoteFunction = objectToUnpack; - return fun; - } else { - throw new Error('data type not recognized by unpack method'); - } -} - export function registerEventHandler( eventHash: string, eventHandler: (event: T) => void @@ -191,21 +148,9 @@ export function unregisterSensor(listenerId: number): void { return NativeReanimatedModule.unregisterSensor(listenerId); } -NativeReanimatedModule.installCoreFunctions(valueUnpacker); - +// initialize UI runtime if applicable if (!isWeb() && isConfigured()) { - const capturableConsole = console; - runOnUI(() => { - 'worklet'; - // @ts-ignore TypeScript doesn't like that there are missing methods in console object, but we don't provide all the methods for the UI runtime console version - global.console = { - debug: runOnJS(capturableConsole.debug), - log: runOnJS(capturableConsole.log), - warn: runOnJS(capturableConsole.warn), - error: runOnJS(capturableConsole.error), - info: runOnJS(capturableConsole.info), - }; - })(); + initializeUIRuntime(); } type FeaturesConfig = { diff --git a/src/reanimated2/errors.ts b/src/reanimated2/errors.ts new file mode 100644 index 00000000000..287214296e9 --- /dev/null +++ b/src/reanimated2/errors.ts @@ -0,0 +1,57 @@ +type StackDetails = [Error, number, number]; + +const _workletStackDetails = new Map(); + +export function registerWorkletStackDetails( + hash: number, + stackDetails: StackDetails +) { + _workletStackDetails.set(hash, stackDetails); +} + +function getBundleOffset(error: Error): [string, number, number] { + const frame = error.stack?.split('\n')?.[0]; + if (frame) { + const parsedFrame = /@([^@]+):(\d+):(\d+)/.exec(frame); + if (parsedFrame) { + const [, file, line, col] = parsedFrame; + return [file, Number(line), Number(col)]; + } + } + return ['unknown', 0, 0]; +} + +function processStack(stack: string): string { + const workletStackEntries = stack.match(/worklet_(\d+):(\d+):(\d+)/g); + let result = stack; + workletStackEntries?.forEach((match) => { + const [, hash, origLine, origCol] = match.split(/:|_/).map(Number); + const errorDetails = _workletStackDetails.get(hash); + if (!errorDetails) { + return; + } + const [error, lineOffset, colOffset] = errorDetails; + const [bundleFile, bundleLine, bundleCol] = getBundleOffset(error); + const line = origLine + bundleLine + lineOffset; + const col = origCol + bundleCol + colOffset; + + result = result.replace(match, `${bundleFile}:${line}:${col}`); + }); + return result; +} + +export function reportFatalErrorOnJS({ + message, + stack, +}: { + message: string; + stack?: string; +}) { + const error = new Error(); + error.message = message; + error.stack = stack ? processStack(stack) : undefined; + error.name = 'ReanimatedError'; + // @ts-ignore React Native's ErrorUtils implementation extends the Error type with jsEngine field + error.jsEngine = 'reanimated'; + global.ErrorUtils.reportFatalError(error); +} diff --git a/src/reanimated2/globals.d.ts b/src/reanimated2/globals.d.ts index c5764c49903..0af77e01543 100644 --- a/src/reanimated2/globals.d.ts +++ b/src/reanimated2/globals.d.ts @@ -18,6 +18,12 @@ declare global { const _frameTimestamp: number | null; const _eventTimestamp: number; const __reanimatedModuleProxy: NativeReanimated; + const evalWithSourceMap: ( + js: string, + sourceURL: string, + sourceMap: string + ) => any; + const evalWithSourceUrl: (js: string, sourceURL: string) => any; const _log: (s: string) => void; const _getCurrentTime: () => number; const _getTimestamp: () => number; @@ -63,6 +69,9 @@ declare global { const ReanimatedDataMock: { now: () => number; }; + const ErrorUtils: { + reportFatalError: (error: Error) => void; + }; const _frameCallbackRegistry: FrameCallbackRegistryUI; const console: Console; @@ -74,6 +83,12 @@ declare global { _frameTimestamp: number | null; _eventTimestamp: number; __reanimatedModuleProxy: NativeReanimated; + evalWithSourceMap: ( + js: string, + sourceURL: string, + sourceMap: string + ) => any; + evalWithSourceUrl: (js: string, sourceURL: string) => any; _log: (s: string) => void; _getCurrentTime: () => number; _getTimestamp: () => number; @@ -116,6 +131,9 @@ declare global { ReanimatedDataMock: { now: () => number; }; + ErrorUtils: { + reportFatalError: (error: Error) => void; + }; _frameCallbackRegistry: FrameCallbackRegistryUI; __workletsCache?: Map any>; __handleCache?: WeakMap; diff --git a/src/reanimated2/initializers.ts b/src/reanimated2/initializers.ts new file mode 100644 index 00000000000..b7d85487bfe --- /dev/null +++ b/src/reanimated2/initializers.ts @@ -0,0 +1,113 @@ +import { reportFatalErrorOnJS } from './errors'; +import NativeReanimatedModule from './NativeReanimated'; +import { runOnUI, runOnJS } from './threads'; + +// callGuard is only used with debug builds +function callGuardDEV, U>( + fn: (...args: T) => U, + ...args: T +): void { + 'worklet'; + try { + fn(...args); + } catch (e) { + if (global.ErrorUtils) { + global.ErrorUtils.reportFatalError(e as Error); + } else { + throw e; + } + } +} + +function valueUnpacker(objectToUnpack: any, category?: string): any { + 'worklet'; + let workletsCache = global.__workletsCache; + let handleCache = global.__handleCache; + if (workletsCache === undefined) { + // init + workletsCache = global.__workletsCache = new Map(); + handleCache = global.__handleCache = new WeakMap(); + } + if (objectToUnpack.__workletHash) { + let workletFun = workletsCache.get(objectToUnpack.__workletHash); + if (workletFun === undefined) { + if (global.evalWithSourceMap) { + // if the runtime (hermes only for now) supports loading source maps + // we want to use the proper filename for the location as it guarantees + // that debugger understands and loads the source code of the file where + // the worklet is defined. + workletFun = global.evalWithSourceMap( + '(' + objectToUnpack.asString + '\n)', + objectToUnpack.__location, + objectToUnpack.__sourceMap + ) as (...args: any[]) => any; + } else if (global.evalWithSourceUrl) { + // if the runtime doesn't support loading source maps, in dev mode we + // can pass source url when evaluating the worklet. Now, instead of using + // the actual file location we use worklet hash, as it the allows us to + // properly symbolicate traces (see errors.ts for details) + workletFun = global.evalWithSourceUrl( + '(' + objectToUnpack.asString + '\n)', + `worklet_${objectToUnpack.__workletHash}` + ) as (...args: any[]) => any; + } else { + // in release we use the regular eval to save on JSI calls + // eslint-disable-next-line no-eval + workletFun = eval('(' + objectToUnpack.asString + '\n)') as ( + ...args: any[] + ) => any; + } + workletsCache.set(objectToUnpack.__workletHash, workletFun); + } + const functionInstance = workletFun.bind(objectToUnpack); + objectToUnpack._recur = functionInstance; + return functionInstance; + } else if (objectToUnpack.__init) { + let value = handleCache!.get(objectToUnpack); + if (value === undefined) { + value = objectToUnpack.__init(); + handleCache!.set(objectToUnpack, value); + } + return value; + } else if (category === 'RemoteFunction') { + const fun = () => { + throw new Error(`Tried to synchronously call a non-worklet function on the UI thread. + +Possible solutions are: + a) If you want to synchronously execute this method, mark it as a worklet + b) If you want to execute this function on the JS thread, wrap it using \`runOnJS\``); + }; + fun.__remoteFunction = objectToUnpack; + return fun; + } else { + throw new Error('data type not recognized by unpack method'); + } +} + +export function initializeUIRuntime() { + NativeReanimatedModule.installCoreFunctions(callGuardDEV, valueUnpacker); + + const capturableConsole = console; + runOnUI(() => { + 'worklet'; + // setup error handler + global.ErrorUtils = { + reportFatalError: (error: Error) => { + runOnJS(reportFatalErrorOnJS)({ + message: error.message, + stack: error.stack, + }); + }, + }; + + // setup console + // @ts-ignore TypeScript doesn't like that there are missing methods in console object, but we don't provide all the methods for the UI runtime console version + global.console = { + debug: runOnJS(capturableConsole.debug), + log: runOnJS(capturableConsole.log), + warn: runOnJS(capturableConsole.warn), + error: runOnJS(capturableConsole.error), + info: runOnJS(capturableConsole.info), + }; + })(); +} diff --git a/src/reanimated2/js-reanimated/JSReanimated.ts b/src/reanimated2/js-reanimated/JSReanimated.ts index 977dc5a2c0c..04ee23bf217 100644 --- a/src/reanimated2/js-reanimated/JSReanimated.ts +++ b/src/reanimated2/js-reanimated/JSReanimated.ts @@ -3,8 +3,6 @@ import { ShareableRef } from '../commonTypes'; import { isJest } from '../PlatformChecker'; export default class JSReanimated extends NativeReanimated { - _valueUnpacker?: (value: T) => void = undefined; - constructor() { super(false); if (isJest()) { @@ -21,8 +19,14 @@ export default class JSReanimated extends NativeReanimated { return { __hostObjectShareableJSRef: value }; } - installCoreFunctions(valueUnpacker: (value: T) => T): void { - this._valueUnpacker = valueUnpacker; + installCoreFunctions( + _callGuard: , U>( + fn: (...args: T) => U, + ...args: T + ) => void, + _valueUnpacker: (value: T) => T + ): void { + // noop } scheduleOnUI(worklet: ShareableRef) { diff --git a/src/reanimated2/shareables.ts b/src/reanimated2/shareables.ts index 84aedc65643..dc7e77c309b 100644 --- a/src/reanimated2/shareables.ts +++ b/src/reanimated2/shareables.ts @@ -1,6 +1,7 @@ import NativeReanimatedModule from './NativeReanimated'; import { ShareableRef } from './commonTypes'; import { shouldBeUseWeb } from './PlatformChecker'; +import { registerWorkletStackDetails } from './errors'; // for web/chrome debugger/jest environments this file provides a stub implementation // where no shareable references are used. Instead, the objects themselves are used @@ -53,6 +54,12 @@ export function makeShareableCloneRecursive(value: any): ShareableRef { // this is a remote function toAdapt = value; } else { + if (__DEV__ && value.__workletHash !== undefined) { + registerWorkletStackDetails( + value.__workletHash, + value.__stackDetails + ); + } toAdapt = {}; for (const [key, element] of Object.entries(value)) { toAdapt[key] = makeShareableCloneRecursive(element);