Files
react-native/ReactCommon/hermes/executor/HermesExecutorFactory.cpp
T
John PortoandFacebook GitHub Bot c81a3c5242 Pass the correct runtime when disabling the debugger
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
2022-07-13 10:28:16 -07:00

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