Skip to content

Commit

Permalink
Enable debugging on Reanimated's runtime using Chrome DevTools (#3526)
Browse files Browse the repository at this point in the history
## 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
Kwasow authored Sep 22, 2022
1 parent 258132d commit 6985391
Show file tree
Hide file tree
Showing 30 changed files with 1,867 additions and 110 deletions.
93 changes: 93 additions & 0 deletions Common/cpp/ReanimatedRuntime/ReanimatedHermesRuntime.cpp
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
125 changes: 125 additions & 0 deletions Common/cpp/ReanimatedRuntime/ReanimatedHermesRuntime.h
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
50 changes: 50 additions & 0 deletions Common/cpp/ReanimatedRuntime/ReanimatedRuntime.cpp
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
27 changes: 27 additions & 0 deletions Common/cpp/ReanimatedRuntime/ReanimatedRuntime.h
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
Loading

0 comments on commit 6985391

Please sign in to comment.