mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Summary: The RN Hermes Inspector keeps a mapping of pageId to Connection. This mapping is created in the call to enableDebugging. Later, when disableDebugging is invoked, the inspector will iterate over the pageId to Connection map to find which page to is being disabled. The DecoratedRuntime enables debugging on construction, and disables on destruction. When disabling debugging, the DecoratedRuntime then passes the "naked" hermesRuntime, which in turn gets checked against all connections' runtimes. It turns our that ```HermesExecutorRuntimeAdapter::getRuntime``` returns the runtime **wrapped** by the DecoratedRuntime, and not hermesRuntime itself. This means that no connections match the request for disabling debugging. Which means the Connection never gets deleted, which in turns keep its underlying adapter (in this case, the ```HermesRuntimeExecutorAdapter```) alive, which has a shared_ptr to the runtime wrapped by the DecoratedRuntime. Other than effectively leaking this connection (and thus the runtime), this also causes an issue when the app tries to re-load the page -- at which point the inspector will remove the old entry from the page to connection mapping, thus initiating the destruction of the (leaked) Runtime in a thread other than the thread that created the runtime. Changelog: [Internal] Reviewed By: javache Differential Revision: D37809467 fbshipit-source-id: e73caa357a300a0d48e69aa3a1a8b00a97519f24
236 lines
7.9 KiB
C++
236 lines
7.9 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<Runtime> runtime,
|
|
HermesRuntime &hermesRuntime,
|
|
std::shared_ptr<MessageQueueThread> thread)
|
|
: runtime_(runtime),
|
|
hermesRuntime_(hermesRuntime),
|
|
thread_(std::move(thread)) {}
|
|
|
|
virtual ~HermesExecutorRuntimeAdapter() = default;
|
|
|
|
jsi::Runtime &getRuntime() override {
|
|
return *runtime_;
|
|
}
|
|
|
|
debugger::Debugger &getDebugger() override {
|
|
return hermesRuntime_.getDebugger();
|
|
}
|
|
|
|
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<Runtime> runtime_;
|
|
HermesRuntime &hermesRuntime_;
|
|
|
|
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)
|
|
: jsi::WithRuntimeDecorator<ReentrancyCheck>(*runtime, reentrancyCheck_),
|
|
runtime_(std::move(runtime)),
|
|
hermesRuntime_(hermesRuntime) {
|
|
#ifdef HERMES_ENABLE_DEBUGGER
|
|
auto adapter = std::make_unique<HermesExecutorRuntimeAdapter>(
|
|
runtime_, hermesRuntime_, jsQueue);
|
|
facebook::hermes::inspector::chrome::enableDebugging(
|
|
std::move(adapter), "Hermes React Native");
|
|
#else
|
|
(void)hermesRuntime_;
|
|
#endif
|
|
}
|
|
|
|
~DecoratedRuntime() {
|
|
#ifdef HERMES_ENABLE_DEBUGGER
|
|
facebook::hermes::inspector::chrome::disableDebugging(*runtime_);
|
|
#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_;
|
|
HermesRuntime &hermesRuntime_;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
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);
|
|
|
|
// 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_);
|
|
}
|
|
|
|
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
|