mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
32d12e89f8
Summary: For brownfield apps, it is possible to have multiple hermes runtimes serving different JS bundles. Hermes inspector currently only supports single JS bundle. The latest loaded JS bundle will overwrite previous JS bundle. This is because we always use the ["Hermes React Native" as the inspector page name](https://github.com/facebook/react-native/blob/de75a7a22eebbe6b7106377bdd697a2d779b91b0/ReactCommon/hermes/executor/HermesExecutorFactory.cpp#L157) and [the latest page name will overwrite previous one](https://github.com/facebook/react-native/blob/de75a7a22eebbe6b7106377bdd697a2d779b91b0/ReactCommon/hermes/inspector/chrome/ConnectionDemux.cpp#L77-L86). This PR adds more customization for HermesExecutorFactory: - `setEnableDebugger`: provide a way to disable debugging features for the hermes runtime - `setDebuggerName`: provide a way to customize inspector page name other than the default "Hermes React Native" ## Changelog [General] [Added] - Add more debugging settings for *HermesExecutorFactory* Pull Request resolved: https://github.com/facebook/react-native/pull/34489 Test Plan: Verify the features by RNTester. 1. `setEnableDebugger` ```diff --- a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.java +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.java @@ -10,10 +10,12 @@ package com.facebook.react.uiapp; import android.app.Application; import androidx.annotation.NonNull; import com.facebook.fbreact.specs.SampleTurboModule; +import com.facebook.hermes.reactexecutor.HermesExecutorFactory; import com.facebook.react.ReactApplication; import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactPackage; import com.facebook.react.TurboReactPackage; +import com.facebook.react.bridge.JavaScriptExecutorFactory; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.config.ReactFeatureFlags; @@ -50,6 +52,13 @@ public class RNTesterApplication extends Application implements ReactApplication return BuildConfig.DEBUG; } + Override + protected JavaScriptExecutorFactory getJavaScriptExecutorFactory() { + HermesExecutorFactory factory = new HermesExecutorFactory(); + factory.setEnableDebugger(false); + return factory; + } + Override public List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( ``` after app launched, the metro inspector should return empty array. Run `curl http://localhost:8081/json` and returns `[]` 2. `setDebuggerName` ```diff --- a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.java +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.java @@ -10,10 +10,12 @@ package com.facebook.react.uiapp; import android.app.Application; import androidx.annotation.NonNull; import com.facebook.fbreact.specs.SampleTurboModule; +import com.facebook.hermes.reactexecutor.HermesExecutorFactory; import com.facebook.react.ReactApplication; import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactPackage; import com.facebook.react.TurboReactPackage; +import com.facebook.react.bridge.JavaScriptExecutorFactory; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.config.ReactFeatureFlags; @@ -50,6 +52,13 @@ public class RNTesterApplication extends Application implements ReactApplication return BuildConfig.DEBUG; } + Override + protected JavaScriptExecutorFactory getJavaScriptExecutorFactory() { + HermesExecutorFactory factory = new HermesExecutorFactory(); + factory.setDebuggerName("Custom Hermes Debugger"); + return factory; + } + Override public List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( ``` after app launched, the metro inspector should return an entry with *Custom Hermes Debugger* Run `curl http://localhost:8081/json` and returns ```json [ { "id": "2-1", "description": "com.facebook.react.uiapp", "title": "Custom Hermes Debugger", "faviconUrl": "https://reactjs.org/favicon.ico", "devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=%5B%3A%3A1%5D%3A8081%2Finspector%2Fdebug%3Fdevice%3D2 (https://github.com/facebook/react-native/commit/e5c5dcd9e26e9443f59864d9763b049e0bda98e7)%26page%3D1 (https://github.com/facebook/react-native/commit/ea93151f21003df6f65dd173dd5dcb3135b0ae94)", "type": "node", "webSocketDebuggerUrl": "ws://[::1]:8081/inspector/debug?device=2&page=1", "vm": "Hermes" } ] ``` Reviewed By: mdvacca Differential Revision: D38982104 Pulled By: cipolleschi fbshipit-source-id: 78003c173db55448a751145986985b3e1d1c71bb
254 lines
8.4 KiB
C++
254 lines
8.4 KiB
C++
/*
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
#include "HermesExecutorFactory.h"
|
|
|
|
#include <thread>
|
|
|
|
#include <cxxreact/MessageQueueThread.h>
|
|
#include <cxxreact/SystraceSection.h>
|
|
#include <hermes/hermes.h>
|
|
#include <jsi/decorator.h>
|
|
|
|
#ifdef HERMES_ENABLE_DEBUGGER
|
|
#include <hermes/inspector/RuntimeAdapter.h>
|
|
#include <hermes/inspector/chrome/Registration.h>
|
|
#endif
|
|
|
|
#include "JSITracing.h"
|
|
|
|
using namespace facebook::hermes;
|
|
using namespace facebook::jsi;
|
|
|
|
namespace facebook {
|
|
namespace react {
|
|
|
|
namespace {
|
|
|
|
std::unique_ptr<HermesRuntime> makeHermesRuntimeSystraced(
|
|
const ::hermes::vm::RuntimeConfig &runtimeConfig) {
|
|
SystraceSection s("HermesExecutorFactory::makeHermesRuntimeSystraced");
|
|
return hermes::makeHermesRuntime(runtimeConfig);
|
|
}
|
|
|
|
#ifdef HERMES_ENABLE_DEBUGGER
|
|
|
|
class HermesExecutorRuntimeAdapter
|
|
: public facebook::hermes::inspector::RuntimeAdapter {
|
|
public:
|
|
HermesExecutorRuntimeAdapter(
|
|
std::shared_ptr<HermesRuntime> runtime,
|
|
std::shared_ptr<MessageQueueThread> thread)
|
|
: runtime_(runtime), thread_(std::move(thread)) {}
|
|
|
|
virtual ~HermesExecutorRuntimeAdapter() = default;
|
|
|
|
HermesRuntime &getRuntime() override {
|
|
return *runtime_;
|
|
}
|
|
|
|
void tickleJs() override {
|
|
// The queue will ensure that runtime_ is still valid when this
|
|
// gets invoked.
|
|
thread_->runOnQueue([&runtime = runtime_]() {
|
|
auto func =
|
|
runtime->global().getPropertyAsFunction(*runtime, "__tickleJs");
|
|
func.call(*runtime);
|
|
});
|
|
}
|
|
|
|
private:
|
|
std::shared_ptr<HermesRuntime> runtime_;
|
|
|
|
std::shared_ptr<MessageQueueThread> thread_;
|
|
};
|
|
|
|
#endif
|
|
|
|
struct ReentrancyCheck {
|
|
// This is effectively a very subtle and complex assert, so only
|
|
// include it in builds which would include asserts.
|
|
#ifndef NDEBUG
|
|
ReentrancyCheck() : 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
|
|
};
|
|
|
|
// This adds ReentrancyCheck and debugger enable/teardown to the given
|
|
// Runtime.
|
|
class DecoratedRuntime : public jsi::WithRuntimeDecorator<ReentrancyCheck> {
|
|
public:
|
|
// The first argument may be another decorater which itself
|
|
// decorates the real HermesRuntime, depending on the build config.
|
|
// The second argument is the real HermesRuntime as well to
|
|
// manage the debugger registration.
|
|
DecoratedRuntime(
|
|
std::unique_ptr<Runtime> runtime,
|
|
HermesRuntime &hermesRuntime,
|
|
std::shared_ptr<MessageQueueThread> jsQueue,
|
|
bool enableDebugger,
|
|
const std::string &debuggerName)
|
|
: jsi::WithRuntimeDecorator<ReentrancyCheck>(*runtime, reentrancyCheck_),
|
|
runtime_(std::move(runtime)) {
|
|
#ifdef HERMES_ENABLE_DEBUGGER
|
|
enableDebugger_ = enableDebugger;
|
|
if (enableDebugger_) {
|
|
std::shared_ptr<HermesRuntime> rt(runtime_, &hermesRuntime);
|
|
auto adapter =
|
|
std::make_unique<HermesExecutorRuntimeAdapter>(rt, jsQueue);
|
|
debugToken_ = facebook::hermes::inspector::chrome::enableDebugging(
|
|
std::move(adapter), debuggerName);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
~DecoratedRuntime() {
|
|
#ifdef HERMES_ENABLE_DEBUGGER
|
|
if (enableDebugger_) {
|
|
facebook::hermes::inspector::chrome::disableDebugging(debugToken_);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
private:
|
|
// runtime_ is a potentially decorated Runtime.
|
|
// hermesRuntime is a reference to a HermesRuntime managed by runtime_.
|
|
//
|
|
// HermesExecutorRuntimeAdapter requirements are kept, because the
|
|
// dtor will disable debugging on the HermesRuntime before the
|
|
// member managing it is destroyed.
|
|
|
|
std::shared_ptr<Runtime> runtime_;
|
|
ReentrancyCheck reentrancyCheck_;
|
|
#ifdef HERMES_ENABLE_DEBUGGER
|
|
bool enableDebugger_;
|
|
facebook::hermes::inspector::chrome::DebugSessionToken debugToken_;
|
|
#endif
|
|
};
|
|
|
|
} // namespace
|
|
|
|
void HermesExecutorFactory::setEnableDebugger(bool enableDebugger) {
|
|
enableDebugger_ = enableDebugger;
|
|
}
|
|
|
|
void HermesExecutorFactory::setDebuggerName(const std::string &debuggerName) {
|
|
debuggerName_ = debuggerName;
|
|
}
|
|
|
|
std::unique_ptr<JSExecutor> HermesExecutorFactory::createJSExecutor(
|
|
std::shared_ptr<ExecutorDelegate> delegate,
|
|
std::shared_ptr<MessageQueueThread> jsQueue) {
|
|
std::unique_ptr<HermesRuntime> hermesRuntime =
|
|
makeHermesRuntimeSystraced(runtimeConfig_);
|
|
HermesRuntime &hermesRuntimeRef = *hermesRuntime;
|
|
auto decoratedRuntime = std::make_shared<DecoratedRuntime>(
|
|
std::move(hermesRuntime),
|
|
hermesRuntimeRef,
|
|
jsQueue,
|
|
enableDebugger_,
|
|
debuggerName_);
|
|
|
|
// So what do we have now?
|
|
// DecoratedRuntime -> HermesRuntime
|
|
//
|
|
// DecoratedRuntime is held by JSIExecutor. When it gets used, it
|
|
// will check that it's on the right thread, do any necessary trace
|
|
// logging, then call the real HermesRuntime. When it is destroyed,
|
|
// it will shut down the debugger before the HermesRuntime is. In
|
|
// the normal case where debugging is not compiled in,
|
|
// all that's left is the thread checking.
|
|
|
|
// Add js engine information to Error.prototype so in error reporting we
|
|
// can send this information.
|
|
auto errorPrototype =
|
|
decoratedRuntime->global()
|
|
.getPropertyAsObject(*decoratedRuntime, "Error")
|
|
.getPropertyAsObject(*decoratedRuntime, "prototype");
|
|
errorPrototype.setProperty(*decoratedRuntime, "jsEngine", "hermes");
|
|
|
|
return std::make_unique<HermesExecutor>(
|
|
decoratedRuntime, delegate, jsQueue, timeoutInvoker_, runtimeInstaller_);
|
|
}
|
|
|
|
::hermes::vm::RuntimeConfig HermesExecutorFactory::defaultRuntimeConfig() {
|
|
return ::hermes::vm::RuntimeConfig::Builder()
|
|
.withEnableSampleProfiling(true)
|
|
.build();
|
|
}
|
|
|
|
HermesExecutor::HermesExecutor(
|
|
std::shared_ptr<jsi::Runtime> runtime,
|
|
std::shared_ptr<ExecutorDelegate> delegate,
|
|
std::shared_ptr<MessageQueueThread> jsQueue,
|
|
const JSIScopedTimeoutInvoker &timeoutInvoker,
|
|
RuntimeInstaller runtimeInstaller)
|
|
: JSIExecutor(runtime, delegate, timeoutInvoker, runtimeInstaller) {
|
|
jsi::addNativeTracingHooks(*runtime);
|
|
}
|
|
|
|
} // namespace react
|
|
} // namespace facebook
|