-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Enable debugging on Reanimated's runtime using Chrome DevTools (#3526)
## Description This PR is related to #1960 (and its replacement #2047). It adds the ability to use Chrome DevTools to add breakpoints and debug worklets (or the UI context in general) both on Android and iOS. ## Major changes Runtime creation has been moved to `ReanimatedRuntime` for both Android and iOS (in the `Common/cpp/ReanimatedRuntime` directory). Before (Android) [file: `NativeProxy.cpp`]: ```cpp #if JS_RUNTIME_HERMES auto config = ::hermes::vm::RuntimeConfig::Builder().withEnableSampleProfiling(false); std::shared_ptr<jsi::Runtime> animatedRuntime = facebook::hermes::makeHermesRuntime(config.build()); #elif JS_RUNTIME_V8 … #else … #endif ``` After (shared) [file: `NativeProxy.cpp`]: ```cpp ReanimatedRuntime::make(jsQueue); ``` The custom build config has been removed, as sample profiling is set to false by default. The created `HermesRuntimeManager` object is the stored inside `ReanimatedNativeModule` as it is important that it’s lifetime is synced with the lifetime of the module. ## Testing - [x] Builds on Android - [x] Builds on Android in release mode - [x] App reloads work on Android - [x] Builds on iOS - [x] Builds on iOS in release mode - [x] App reloads work on iOS - [x] JSC still works - [x] JSC debugging in Safari with iOS still works - [x] Paper still works Versions of React-Native tested: 0.70 ## Things to look into - ~~Breakpoint labels do not appear on iOS~~ _I tested this on a new app with Flipper on the main JS thread, and it was also the case, so it seem to not be an issue on our side_ - ~~After removing a breakpoint on iOS it can't be set in the same location again~~ _This seems to be an issue with Chrome DevTools as connecting to Reanimated's runtime through Flipper fully works_ - ~~Edge-case: the app will crash if a reload is performed while the debugger is open~~ _Will be fixed in a PR to metro and [58e9e7b](https://github.com/software-mansion/react-native-reanimated/pull/3526/commits/58e9e7bcd7d3b6b590a5f6fca0db93a951eaa39e)_ - The latest release of Chrome (105) broke source maps, so only Flipper works now. I reported the issue [here](https://bugs.chromium.org/p/chromium/issues/detail?id=1360298#makechanges), and I'm waiting for a response from the Chromium team ## TODO - [x] Open PR to update debugging docs in documentation (will be included in #3446) - [x] Open PR in [facebook/flipper](https://github.com/facebook/flipper) to enable Flipper support on custom runtimes (facebook/flipper#4047) - [x] Open PR in [facebook/metro](https://github.com/facebook/metro) to support debugger reloads on custom runtimes (facebook/metro#864)
- Loading branch information
Showing
30 changed files
with
1,867 additions
and
110 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
#include "ReanimatedHermesRuntime.h" | ||
|
||
// Only include this file in Hermes-enabled builds as some platforms (like tvOS) | ||
// don't support hermes and it causes the compilation to fail. | ||
#if JS_RUNTIME_HERMES | ||
|
||
#include <cxxreact/MessageQueueThread.h> | ||
#include <jsi/decorator.h> | ||
#include <jsi/jsi.h> | ||
|
||
#include <memory> | ||
#include <utility> | ||
|
||
#if __has_include(<reacthermes/HermesExecutorFactory.h>) | ||
#include <reacthermes/HermesExecutorFactory.h> | ||
#else // __has_include(<hermes/hermes.h>) || ANDROID | ||
#include <hermes/hermes.h> | ||
#endif | ||
|
||
#include <hermes/inspector/RuntimeAdapter.h> | ||
#include <hermes/inspector/chrome/Registration.h> | ||
|
||
namespace reanimated { | ||
|
||
using namespace facebook; | ||
using namespace react; | ||
|
||
#if HERMES_ENABLE_DEBUGGER | ||
|
||
class HermesExecutorRuntimeAdapter | ||
: public facebook::hermes::inspector::RuntimeAdapter { | ||
public: | ||
HermesExecutorRuntimeAdapter( | ||
facebook::hermes::HermesRuntime &hermesRuntime, | ||
std::shared_ptr<MessageQueueThread> thread) | ||
: hermesRuntime_(hermesRuntime), thread_(std::move(thread)) {} | ||
|
||
virtual ~HermesExecutorRuntimeAdapter() { | ||
// This is required by iOS, because there is an assertion in the destructor | ||
// that the thread was indeed `quit` before | ||
thread_->quitSynchronous(); | ||
} | ||
|
||
facebook::jsi::Runtime &getRuntime() override { | ||
return hermesRuntime_; | ||
} | ||
|
||
facebook::hermes::debugger::Debugger &getDebugger() override { | ||
return hermesRuntime_.getDebugger(); | ||
} | ||
|
||
// This is not empty in the original implementation, but we decided to tickle | ||
// the runtime by running a small piece of code on every frame as using this | ||
// required us to hold a refernce to the runtime inside this adapter which | ||
// caused issues while reloading the app. | ||
void tickleJs() override {} | ||
|
||
public: | ||
facebook::hermes::HermesRuntime &hermesRuntime_; | ||
std::shared_ptr<MessageQueueThread> thread_; | ||
}; | ||
|
||
#endif // HERMES_ENABLE_DEBUGGER | ||
|
||
ReanimatedHermesRuntime::ReanimatedHermesRuntime( | ||
std::unique_ptr<facebook::hermes::HermesRuntime> runtime, | ||
std::shared_ptr<MessageQueueThread> jsQueue) | ||
: jsi::WithRuntimeDecorator<ReanimatedReentrancyCheck>( | ||
*runtime, | ||
reentrancyCheck_), | ||
runtime_(std::move(runtime)) { | ||
#if HERMES_ENABLE_DEBUGGER | ||
auto adapter = | ||
std::make_unique<HermesExecutorRuntimeAdapter>(*runtime_, jsQueue); | ||
facebook::hermes::inspector::chrome::enableDebugging( | ||
std::move(adapter), "Reanimated Runtime"); | ||
#else | ||
// This is required by iOS, because there is an assertion in the destructor | ||
// that the thread was indeed `quit` before | ||
jsQueue->quitSynchronous(); | ||
#endif | ||
} | ||
|
||
ReanimatedHermesRuntime::~ReanimatedHermesRuntime() { | ||
#if HERMES_ENABLE_DEBUGGER | ||
// We have to disable debugging before the runtime is destroyed. | ||
facebook::hermes::inspector::chrome::disableDebugging(*runtime_); | ||
#endif | ||
} | ||
|
||
} // namespace reanimated | ||
|
||
#endif // JS_RUNTIME_HERMES |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
#pragma once | ||
|
||
// JS_RUNTIME_HERMES is only set on Android so we have to check __has_include | ||
// on iOS. | ||
#if __APPLE__ && \ | ||
(__has_include( \ | ||
<reacthermes/HermesExecutorFactory.h>) || __has_include(<hermes/hermes.h>)) | ||
#define JS_RUNTIME_HERMES 1 | ||
#endif | ||
|
||
// Only include this file in Hermes-enabled builds as some platforms (like tvOS) | ||
// don't support hermes and it causes the compilation to fail. | ||
#if JS_RUNTIME_HERMES | ||
|
||
#include <cxxreact/MessageQueueThread.h> | ||
#include <jsi/decorator.h> | ||
#include <jsi/jsi.h> | ||
|
||
#include <memory> | ||
#include <thread> | ||
|
||
#if __has_include(<reacthermes/HermesExecutorFactory.h>) | ||
#include <reacthermes/HermesExecutorFactory.h> | ||
#else // __has_include(<hermes/hermes.h>) || ANDROID | ||
#include <hermes/hermes.h> | ||
#endif | ||
|
||
namespace reanimated { | ||
|
||
using namespace facebook; | ||
using namespace react; | ||
|
||
// ReentrancyCheck is copied from React Native | ||
// from ReactCommon/hermes/executor/HermesExecutorFactory.cpp | ||
// https://github.com/facebook/react-native/blob/main/ReactCommon/hermes/executor/HermesExecutorFactory.cpp | ||
struct ReanimatedReentrancyCheck { | ||
// This is effectively a very subtle and complex assert, so only | ||
// include it in builds which would include asserts. | ||
#ifndef NDEBUG | ||
ReanimatedReentrancyCheck() : tid(std::thread::id()), depth(0) {} | ||
|
||
void before() { | ||
std::thread::id this_id = std::this_thread::get_id(); | ||
std::thread::id expected = std::thread::id(); | ||
|
||
// A note on memory ordering: the main purpose of these checks is | ||
// to observe a before/before race, without an intervening after. | ||
// This will be detected by the compare_exchange_strong atomicity | ||
// properties, regardless of memory order. | ||
// | ||
// For everything else, it is easiest to think of 'depth' as a | ||
// proxy for any access made inside the VM. If access to depth | ||
// are reordered incorrectly, the same could be true of any other | ||
// operation made by the VM. In fact, using acquire/release | ||
// memory ordering could create barriers which mask a programmer | ||
// error. So, we use relaxed memory order, to avoid masking | ||
// actual ordering errors. Although, in practice, ordering errors | ||
// of this sort would be surprising, because the decorator would | ||
// need to call after() without before(). | ||
|
||
if (tid.compare_exchange_strong( | ||
expected, this_id, std::memory_order_relaxed)) { | ||
// Returns true if tid and expected were the same. If they | ||
// were, then the stored tid referred to no thread, and we | ||
// atomically saved this thread's tid. Now increment depth. | ||
assert(depth == 0 && "No thread id, but depth != 0"); | ||
++depth; | ||
} else if (expected == this_id) { | ||
// If the stored tid referred to a thread, expected was set to | ||
// that value. If that value is this thread's tid, that's ok, | ||
// just increment depth again. | ||
assert(depth != 0 && "Thread id was set, but depth == 0"); | ||
++depth; | ||
} else { | ||
// The stored tid was some other thread. This indicates a bad | ||
// programmer error, where VM methods were called on two | ||
// different threads unsafely. Fail fast (and hard) so the | ||
// crash can be analyzed. | ||
__builtin_trap(); | ||
} | ||
} | ||
|
||
void after() { | ||
assert( | ||
tid.load(std::memory_order_relaxed) == std::this_thread::get_id() && | ||
"No thread id in after()"); | ||
if (--depth == 0) { | ||
// If we decremented depth to zero, store no-thread into tid. | ||
std::thread::id expected = std::this_thread::get_id(); | ||
bool didWrite = tid.compare_exchange_strong( | ||
expected, std::thread::id(), std::memory_order_relaxed); | ||
assert(didWrite && "Decremented to zero, but no tid write"); | ||
} | ||
} | ||
|
||
std::atomic<std::thread::id> tid; | ||
// This is not atomic, as it is only written or read from the owning | ||
// thread. | ||
unsigned int depth; | ||
#endif // NDEBUG | ||
}; | ||
|
||
// This is in fact a subclass of jsi::Runtime! WithRuntimeDecorator is a | ||
// template class that is a subclass of DecoratedRuntime which is also a | ||
// template class that then inherits its template, which in this case is | ||
// jsi::Runtime. So the inheritance is: ReanimatedHermesRuntime -> | ||
// WithRuntimeDecorator -> DecoratedRuntime -> jsi::Runtime You can find out | ||
// more about this in ReactCommon/jsi/jsi/Decorator.h or by following this link: | ||
// https://github.com/facebook/react-native/blob/main/ReactCommon/jsi/jsi/decorator.h | ||
class ReanimatedHermesRuntime | ||
: public jsi::WithRuntimeDecorator<ReanimatedReentrancyCheck> { | ||
public: | ||
ReanimatedHermesRuntime( | ||
std::unique_ptr<facebook::hermes::HermesRuntime> runtime, | ||
std::shared_ptr<MessageQueueThread> jsQueue); | ||
~ReanimatedHermesRuntime(); | ||
|
||
private: | ||
std::shared_ptr<facebook::hermes::HermesRuntime> runtime_; | ||
ReanimatedReentrancyCheck reentrancyCheck_; | ||
}; | ||
|
||
} // namespace reanimated | ||
|
||
#endif // JS_RUNTIME_HERMES |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
#include "ReanimatedRuntime.h" | ||
|
||
#include <cxxreact/MessageQueueThread.h> | ||
#include <jsi/jsi.h> | ||
|
||
#include <memory> | ||
#include <utility> | ||
|
||
#if JS_RUNTIME_HERMES | ||
#include "ReanimatedHermesRuntime.h" | ||
#elif JS_RUNTIME_V8 | ||
#include <v8runtime/V8RuntimeFactory.h> | ||
#else | ||
#include <jsi/JSCRuntime.h> | ||
#endif | ||
|
||
namespace reanimated { | ||
|
||
using namespace facebook; | ||
using namespace react; | ||
|
||
std::shared_ptr<jsi::Runtime> ReanimatedRuntime::make( | ||
std::shared_ptr<MessageQueueThread> jsQueue) { | ||
#if JS_RUNTIME_HERMES | ||
std::unique_ptr<facebook::hermes::HermesRuntime> runtime = | ||
facebook::hermes::makeHermesRuntime(); | ||
|
||
// We don't call `jsQueue->quitSynchronous()` here, since it will be done | ||
// later in ReanimatedHermesRuntime | ||
|
||
return std::make_shared<ReanimatedHermesRuntime>(std::move(runtime), jsQueue); | ||
#elif JS_RUNTIME_V8 | ||
// This is required by iOS, because there is an assertion in the destructor | ||
// that the thread was indeed `quit` before. | ||
jsQueue->quitSynchronous(); | ||
|
||
auto config = std::make_unique<rnv8::V8RuntimeConfig>(); | ||
config->enableInspector = false; | ||
config->appName = "reanimated"; | ||
return rnv8::createSharedV8Runtime(runtime_, std::move(config)); | ||
#else | ||
// This is required by iOS, because there is an assertion in the destructor | ||
// that the thread was indeed `quit` before | ||
jsQueue->quitSynchronous(); | ||
|
||
return facebook::jsc::makeJSCRuntime(); | ||
#endif | ||
} | ||
|
||
} // namespace reanimated |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
#pragma once | ||
|
||
// JS_RUNTIME_HERMES is only set on Android so we have to check __has_include | ||
// on iOS. | ||
#if __APPLE__ && \ | ||
(__has_include( \ | ||
<reacthermes/HermesExecutorFactory.h>) || __has_include(<hermes/hermes.h>)) | ||
#define JS_RUNTIME_HERMES 1 | ||
#endif | ||
|
||
#include <cxxreact/MessageQueueThread.h> | ||
#include <jsi/jsi.h> | ||
|
||
#include <memory> | ||
|
||
namespace reanimated { | ||
|
||
using namespace facebook; | ||
using namespace react; | ||
|
||
class ReanimatedRuntime { | ||
public: | ||
static std::shared_ptr<jsi::Runtime> make( | ||
std::shared_ptr<MessageQueueThread> jsQueue); | ||
}; | ||
|
||
} // namespace reanimated |
Oops, something went wrong.