first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,143 @@
/*
* 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 "ConnectionDemux.h"
#if defined(HERMES_ENABLE_DEBUGGER) && !defined(HERMES_V1_ENABLED)
#include <hermes/inspector/RuntimeAdapter.h>
#include <hermes/inspector/chrome/CDPHandler.h>
#include <jsinspector-modern/InspectorInterfaces.h>
#include <utility>
namespace facebook::hermes::inspector_modern::chrome {
using ::facebook::react::jsinspector_modern::ILocalConnection;
using ::facebook::react::jsinspector_modern::IRemoteConnection;
namespace {
class LocalConnection : public ILocalConnection {
public:
LocalConnection(
std::shared_ptr<hermes::inspector_modern::chrome::CDPHandler> conn,
std::shared_ptr<std::unordered_set<std::string>> inspectedContexts);
~LocalConnection() override;
void sendMessage(std::string message) override;
void disconnect() override;
private:
std::shared_ptr<hermes::inspector_modern::chrome::CDPHandler> conn_;
std::shared_ptr<std::unordered_set<std::string>> inspectedContexts_;
};
LocalConnection::LocalConnection(
std::shared_ptr<hermes::inspector_modern::chrome::CDPHandler> conn,
std::shared_ptr<std::unordered_set<std::string>> inspectedContexts)
: conn_(conn), inspectedContexts_(std::move(inspectedContexts)) {
inspectedContexts_->insert(conn->getTitle());
}
LocalConnection::~LocalConnection() = default;
void LocalConnection::sendMessage(std::string message) {
conn_->handle(std::move(message));
}
void LocalConnection::disconnect() {
inspectedContexts_->erase(conn_->getTitle());
conn_->unregisterCallbacks();
}
} // namespace
ConnectionDemux::ConnectionDemux(
facebook::react::jsinspector_modern::IInspector& inspector)
: globalInspector_(inspector),
inspectedContexts_(std::make_shared<std::unordered_set<std::string>>()) {}
ConnectionDemux::~ConnectionDemux() = default;
DebugSessionToken ConnectionDemux::enableDebugging(
std::unique_ptr<RuntimeAdapter> adapter,
const std::string& title) {
std::scoped_lock lock(mutex_);
// TODO(#22976087): workaround for ComponentScript contexts never being
// destroyed.
//
// After a reload, the old ComponentScript VM instance stays alive. When we
// register the new CS VM instance, check for any previous CS VM (via strcmp
// of title) and remove them.
std::vector<int> pagesToDelete;
for (auto& conn : conns_) {
if (conn.second->getTitle() == title) {
pagesToDelete.push_back(conn.first);
}
}
for (auto pageId : pagesToDelete) {
removePage(pageId);
}
auto waitForDebugger =
(inspectedContexts_->find(title) != inspectedContexts_->end());
return addPage(
hermes::inspector_modern::chrome::CDPHandler::create(
std::move(adapter), title, waitForDebugger));
}
void ConnectionDemux::disableDebugging(DebugSessionToken session) {
std::scoped_lock lock(mutex_);
if (conns_.find(session) == conns_.end()) {
return;
}
removePage(session);
}
int ConnectionDemux::addPage(
std::shared_ptr<hermes::inspector_modern::chrome::CDPHandler> conn) {
auto connectFunc = [conn, this](std::unique_ptr<IRemoteConnection> remoteConn)
-> std::unique_ptr<ILocalConnection> {
// This cannot be unique_ptr as std::function is copyable but unique_ptr
// isn't. TODO: Change the CDPHandler API to accommodate this and not
// require a copyable callback?
std::shared_ptr<IRemoteConnection> sharedConn = std::move(remoteConn);
if (!conn->registerCallbacks(
[sharedConn](const std::string& message) {
sharedConn->onMessage(message);
},
[sharedConn]() { sharedConn->onDisconnect(); })) {
return nullptr;
}
return std::make_unique<LocalConnection>(conn, inspectedContexts_);
};
int pageId = globalInspector_.addPage(
conn->getTitle(), "Hermes", std::move(connectFunc));
conns_[pageId] = std::move(conn);
return pageId;
}
void ConnectionDemux::removePage(int pageId) {
globalInspector_.removePage(pageId);
auto conn = conns_.at(pageId);
std::string title = conn->getTitle();
inspectedContexts_->erase(title);
conn->unregisterCallbacks();
conns_.erase(pageId);
}
} // namespace facebook::hermes::inspector_modern::chrome
#endif // defined(HERMES_ENABLE_DEBUGGER) && !defined(HERMES_V1_ENABLED)

View File

@@ -0,0 +1,55 @@
/*
* 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.
*/
#pragma once
#if defined(HERMES_ENABLE_DEBUGGER) && !defined(HERMES_V1_ENABLED)
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <hermes/hermes.h>
#include <hermes/inspector-modern/chrome/Registration.h>
#include <hermes/inspector/RuntimeAdapter.h>
#include <hermes/inspector/chrome/CDPHandler.h>
#include <jsinspector-modern/InspectorInterfaces.h>
namespace facebook::hermes::inspector_modern::chrome {
/*
* ConnectionDemux keeps track of all debuggable Hermes runtimes (called
* "pages" in the higher-level React Native API) in this process. See
* Registration.h for documentation of the public API.
*/
class ConnectionDemux {
public:
explicit ConnectionDemux(facebook::react::jsinspector_modern::IInspector &inspector);
~ConnectionDemux();
ConnectionDemux(const ConnectionDemux &) = delete;
ConnectionDemux &operator=(const ConnectionDemux &) = delete;
DebugSessionToken enableDebugging(std::unique_ptr<RuntimeAdapter> adapter, const std::string &title);
void disableDebugging(DebugSessionToken session);
private:
int addPage(std::shared_ptr<hermes::inspector_modern::chrome::CDPHandler> conn);
void removePage(int pageId);
facebook::react::jsinspector_modern::IInspector &globalInspector_;
std::mutex mutex_;
std::unordered_map<int, std::shared_ptr<hermes::inspector_modern::chrome::CDPHandler>> conns_;
std::shared_ptr<std::unordered_set<std::string>> inspectedContexts_;
};
} // namespace facebook::hermes::inspector_modern::chrome
#endif // defined(HERMES_ENABLE_DEBUGGER) && !defined(HERMES_V1_ENABLED)

View File

@@ -0,0 +1,129 @@
/*
* 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.
*/
#ifdef HERMES_ENABLE_DEBUGGER
#include "HermesRuntimeAgentDelegate.h"
#include <hermes/AsyncDebuggerAPI.h>
#include <hermes/cdp/CDPAgent.h>
#include <hermes/hermes.h>
#include <jsinspector-modern/ReactCdp.h>
#include <utility>
using namespace facebook::hermes;
namespace facebook::react::jsinspector_modern {
class HermesRuntimeAgentDelegate::Impl final : public RuntimeAgentDelegate {
using HermesState = hermes::cdp::State;
struct HermesStateWrapper : public ExportedState {
explicit HermesStateWrapper(HermesState state) : state_(std::move(state)) {}
static HermesState unwrapDestructively(ExportedState* wrapper) {
if (wrapper == nullptr) {
return {};
}
if (auto* typedWrapper = dynamic_cast<HermesStateWrapper*>(wrapper)) {
return std::move(typedWrapper->state_);
}
return {};
}
private:
HermesState state_;
};
public:
Impl(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>
previouslyExportedState,
const ExecutionContextDescription& executionContextDescription,
HermesRuntime& runtime,
HermesRuntimeTargetDelegate& runtimeTargetDelegate,
const RuntimeExecutor& runtimeExecutor)
: hermes_(
hermes::cdp::CDPAgent::create(
executionContextDescription.id,
runtimeTargetDelegate.getCDPDebugAPI(),
// RuntimeTask takes a HermesRuntime whereas our RuntimeExecutor
// takes a jsi::Runtime.
[runtimeExecutor,
&runtime](facebook::hermes::debugger::RuntimeTask fn) {
runtimeExecutor(
[&runtime, fn = std::move(fn)](auto&) { fn(runtime); });
},
std::move(frontendChannel),
HermesStateWrapper::unwrapDestructively(
previouslyExportedState.get()))) {
if (sessionState.isRuntimeDomainEnabled) {
hermes_->enableRuntimeDomain();
}
if (sessionState.isDebuggerDomainEnabled) {
hermes_->enableDebuggerDomain();
}
}
bool handleRequest(const cdp::PreparsedRequest& req) override {
if (req.method.starts_with("Log.") || req.method.starts_with("Network.")) {
// Since we know Hermes doesn't do anything useful with Log or Network
// messages, but our containing HostAgent will, bail out early.
// TODO: We need a way to negotiate this more dynamically with Hermes
// through the API.
return false;
}
// Forward everything else to Hermes's CDPAgent.
hermes_->handleCommand(req.toJson());
// Let the call know that this request is handled (i.e. it is Hermes's
// responsibility to respond with either success or an error).
return true;
}
std::unique_ptr<ExportedState> getExportedState() override {
return std::make_unique<HermesStateWrapper>(hermes_->getState());
}
private:
std::unique_ptr<hermes::cdp::CDPAgent> hermes_;
};
HermesRuntimeAgentDelegate::HermesRuntimeAgentDelegate(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>
previouslyExportedState,
const ExecutionContextDescription& executionContextDescription,
HermesRuntime& runtime,
HermesRuntimeTargetDelegate& runtimeTargetDelegate,
RuntimeExecutor runtimeExecutor)
: impl_(
std::make_unique<Impl>(
std::move(frontendChannel),
sessionState,
std::move(previouslyExportedState),
executionContextDescription,
runtime,
runtimeTargetDelegate,
std::move(runtimeExecutor))) {}
bool HermesRuntimeAgentDelegate::handleRequest(
const cdp::PreparsedRequest& req) {
return impl_->handleRequest(req);
}
std::unique_ptr<RuntimeAgentDelegate::ExportedState>
HermesRuntimeAgentDelegate::getExportedState() {
return impl_->getExportedState();
}
} // namespace facebook::react::jsinspector_modern
#endif // HERMES_ENABLE_DEBUGGER

View File

@@ -0,0 +1,80 @@
/*
* 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.
*/
#pragma once
#ifdef HERMES_ENABLE_DEBUGGER
#include "HermesRuntimeTargetDelegate.h"
#include <ReactCommon/RuntimeExecutor.h>
#include <hermes/hermes.h>
#include <jsinspector-modern/ReactCdp.h>
namespace facebook::react::jsinspector_modern {
/**
* A RuntimeAgentDelegate that handles requests from the Chrome DevTools
* Protocol for an instance of Hermes, using the modern CDPAgent API.
*/
class HermesRuntimeAgentDelegate : public RuntimeAgentDelegate {
public:
/**
* \param frontendChannel A channel used to send responses and events to the
* frontend.
* \param sessionState The state of the current CDP session. This will only
* be accessed on the main thread (during the constructor, in handleRequest,
* etc).
* \param previouslyExportedState The exported state from a previous instance
* of RuntimeAgentDelegate (NOT necessarily HermesRuntimeAgentDelegate).
* This may be nullptr, and if not nullptr it may be of any concrete type that
* implements RuntimeAgentDelegate::ExportedState.
* \param executionContextDescription A description of the execution context
* represented by this runtime. This is used for disambiguating the
* source/destination of CDP messages when there are multiple runtimes
* (concurrently or over the life of a Host).
* \param runtime The HermesRuntime that this agent is attached to. The caller
* is responsible for keeping this object alive for the duration of the
* \c HermesRuntimeAgentDelegate lifetime.
* \param runtimeTargetDelegate The \c HermesRuntimeTargetDelegate object
* object for the passed runtime.
* \param runtimeExecutor A callback for scheduling work on the JS thread.
* \c runtimeExecutor may drop scheduled work if the runtime is destroyed
* first.
*/
HermesRuntimeAgentDelegate(
FrontendChannel frontendChannel,
SessionState &sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState> previouslyExportedState,
const ExecutionContextDescription &executionContextDescription,
hermes::HermesRuntime &runtime,
HermesRuntimeTargetDelegate &runtimeTargetDelegate,
RuntimeExecutor runtimeExecutor);
/**
* Handle a CDP request. The response will be sent over the provided
* \c FrontendChannel synchronously or asynchronously.
* \param req The parsed request.
* \returns true if this agent has responded, or will respond asynchronously,
* to the request (with either a success or error message). False if the
* agent expects another agent to respond to the request instead.
*/
bool handleRequest(const cdp::PreparsedRequest &req) override;
std::unique_ptr<RuntimeAgentDelegate::ExportedState> getExportedState() override;
private:
class Impl;
const std::unique_ptr<Impl> impl_;
};
} // namespace facebook::react::jsinspector_modern
#else
#error "HERMES_ENABLE_DEBUGGER must be enabled to use HermesRuntimeAgentDelegate."
#endif // HERMES_ENABLE_DEBUGGER

View File

@@ -0,0 +1,176 @@
/*
* 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 "HermesRuntimeSamplingProfileSerializer.h"
#include <oscompat/OSCompat.h>
#include <variant>
namespace facebook::react::jsinspector_modern::tracing {
namespace {
namespace fhsp = facebook::hermes::sampling_profiler;
/// Fallback script ID for call frames, when Hermes didn't provide one or when
/// this frame is part of the VM, like native functions, used for parity with
/// Chromium + V8.
constexpr uint32_t FALLBACK_SCRIPT_ID = 0;
/// Garbage collector frame name, used for parity with Chromium + V8.
constexpr std::string_view GARBAGE_COLLECTOR_FRAME_NAME = "(garbage collector)";
/// Filters out Hermes Suspend frames related to Debugger.
/// Even though Debugger domain is expected to be disabled, Hermes might run
/// Debugger loop while recording sampling profile. We only allow GC frames.
inline bool shouldIgnoreHermesFrame(
const fhsp::ProfileSampleCallStackSuspendFrame& suspendFrame) {
return suspendFrame.getSuspendFrameKind() !=
fhsp::ProfileSampleCallStackSuspendFrame::SuspendFrameKind::GC;
}
RuntimeSamplingProfile::SampleCallStackFrame convertNativeHermesFrame(
const fhsp::ProfileSampleCallStackNativeFunctionFrame& frame) {
return RuntimeSamplingProfile::SampleCallStackFrame{
.kind =
RuntimeSamplingProfile::SampleCallStackFrame::Kind::NativeFunction,
.scriptId =
FALLBACK_SCRIPT_ID, // JavaScript Runtime defines the implementation
// for native function, no script ID to reference.
.functionName = frame.getFunctionName(),
};
}
RuntimeSamplingProfile::SampleCallStackFrame convertHostFunctionHermesFrame(
const fhsp::ProfileSampleCallStackHostFunctionFrame& frame) {
return RuntimeSamplingProfile::SampleCallStackFrame{
.kind = RuntimeSamplingProfile::SampleCallStackFrame::Kind::HostFunction,
.scriptId =
FALLBACK_SCRIPT_ID, // JavaScript Runtime defines the implementation
// for host function, no script ID to reference.
.functionName = frame.getFunctionName(),
};
}
RuntimeSamplingProfile::SampleCallStackFrame convertSuspendHermesFrame(
const fhsp::ProfileSampleCallStackSuspendFrame& frame) {
if (frame.getSuspendFrameKind() ==
fhsp::ProfileSampleCallStackSuspendFrame::SuspendFrameKind::GC) {
return RuntimeSamplingProfile::SampleCallStackFrame{
.kind = RuntimeSamplingProfile::SampleCallStackFrame::Kind::
GarbageCollector,
.scriptId = FALLBACK_SCRIPT_ID, // GC frames are part of the VM, no
// script ID to reference.
.functionName = GARBAGE_COLLECTOR_FRAME_NAME,
};
}
// We should have filtered out Debugger Suspend frames before in
// shouldFilterOutHermesFrame().
throw std::logic_error{"Unexpected Suspend frame found in Hermes call stack"};
}
RuntimeSamplingProfile::SampleCallStackFrame convertJSFunctionHermesFrame(
const fhsp::ProfileSampleCallStackJSFunctionFrame& frame) {
return RuntimeSamplingProfile::SampleCallStackFrame{
.kind = RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction,
.scriptId = frame.getScriptId(),
.functionName = frame.getFunctionName(),
.scriptURL = frame.hasScriptUrl()
? std::optional<std::string_view>{frame.getScriptUrl()}
: std::nullopt,
.lineNumber = frame.hasFunctionLineNumber()
? std::optional<uint32_t>{frame.getFunctionLineNumber() - 1}
// Hermes VM keeps line numbers as 1-based. Convert
// to 0-based.
: std::nullopt,
.columnNumber = frame.hasFunctionColumnNumber()
? std::optional<uint32_t>{frame.getFunctionColumnNumber() - 1}
// Hermes VM keeps column numbers as 1-based. Convert to
// 0-based.
: std::nullopt,
};
}
RuntimeSamplingProfile::Sample convertHermesSampleToTracingSample(
const fhsp::ProfileSample& hermesSample) {
uint64_t reconciledTimestamp = hermesSample.getTimestamp();
const auto callStackRange = hermesSample.getCallStackFramesRange();
std::vector<RuntimeSamplingProfile::SampleCallStackFrame>
reconciledSampleCallStack;
reconciledSampleCallStack.reserve(hermesSample.getCallStackFramesCount());
for (const auto& hermesFrame : callStackRange) {
if (std::holds_alternative<fhsp::ProfileSampleCallStackSuspendFrame>(
hermesFrame)) {
const auto& suspendFrame =
std::get<fhsp::ProfileSampleCallStackSuspendFrame>(hermesFrame);
if (shouldIgnoreHermesFrame(suspendFrame)) {
continue;
}
reconciledSampleCallStack.emplace_back(
convertSuspendHermesFrame(suspendFrame));
} else if (std::holds_alternative<
fhsp::ProfileSampleCallStackNativeFunctionFrame>(
hermesFrame)) {
const auto& nativeFunctionFrame =
std::get<fhsp::ProfileSampleCallStackNativeFunctionFrame>(
hermesFrame);
reconciledSampleCallStack.emplace_back(
convertNativeHermesFrame(nativeFunctionFrame));
} else if (std::holds_alternative<
fhsp::ProfileSampleCallStackHostFunctionFrame>(
hermesFrame)) {
const auto& hostFunctionFrame =
std::get<fhsp::ProfileSampleCallStackHostFunctionFrame>(hermesFrame);
reconciledSampleCallStack.emplace_back(
convertHostFunctionHermesFrame(hostFunctionFrame));
} else if (std::holds_alternative<
fhsp::ProfileSampleCallStackJSFunctionFrame>(hermesFrame)) {
const auto& jsFunctionFrame =
std::get<fhsp::ProfileSampleCallStackJSFunctionFrame>(hermesFrame);
reconciledSampleCallStack.emplace_back(
convertJSFunctionHermesFrame(jsFunctionFrame));
} else {
throw std::logic_error{"Unknown Hermes stack frame kind"};
}
}
return RuntimeSamplingProfile::Sample{
reconciledTimestamp,
hermesSample.getThreadId(),
std::move(reconciledSampleCallStack)};
}
} // namespace
/* static */ RuntimeSamplingProfile
HermesRuntimeSamplingProfileSerializer::serializeToTracingSamplingProfile(
hermes::sampling_profiler::Profile hermesProfile) {
const auto samplesRange = hermesProfile.getSamplesRange();
std::vector<RuntimeSamplingProfile::Sample> reconciledSamples;
reconciledSamples.reserve(hermesProfile.getSamplesCount());
for (const auto& hermesSample : samplesRange) {
RuntimeSamplingProfile::Sample reconciledSample =
convertHermesSampleToTracingSample(hermesSample);
reconciledSamples.push_back(std::move(reconciledSample));
}
return RuntimeSamplingProfile{
"Hermes",
// Hermes' Profile should be the source of truth for this,
// but it is safe to reuse the process ID here, since everything runs in
// the same process.
oscompat::getCurrentProcessId(),
std::move(reconciledSamples),
std::make_unique<RawHermesRuntimeProfile>(std::move(hermesProfile))};
}
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,33 @@
/*
* 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.
*/
#pragma once
#include <hermes/hermes.h>
#include <jsinspector-modern/tracing/RuntimeSamplingProfile.h>
namespace facebook::react::jsinspector_modern::tracing {
class RawHermesRuntimeProfile : public RawRuntimeProfile {
public:
explicit RawHermesRuntimeProfile(hermes::sampling_profiler::Profile hermesProfile)
: hermesProfile_{std::move(hermesProfile)}
{
}
private:
hermes::sampling_profiler::Profile hermesProfile_;
};
class HermesRuntimeSamplingProfileSerializer {
public:
static tracing::RuntimeSamplingProfile serializeToTracingSamplingProfile(
hermes::sampling_profiler::Profile hermesProfile);
};
} // namespace facebook::react::jsinspector_modern::tracing

View File

@@ -0,0 +1,374 @@
/*
* 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 <jsi/jsi.h>
#include <jsinspector-modern/RuntimeTarget.h>
#include "HermesRuntimeSamplingProfileSerializer.h"
#include "HermesRuntimeTargetDelegate.h"
// If HERMES_ENABLE_DEBUGGER isn't defined, we can't access any Hermes
// CDPHandler headers or types.
#ifdef HERMES_ENABLE_DEBUGGER
#include "HermesRuntimeAgentDelegate.h"
#include <hermes/cdp/CDPDebugAPI.h>
using namespace facebook::hermes::cdp;
#else
#include <jsinspector-modern/FallbackRuntimeTargetDelegate.h>
#endif // HERMES_ENABLE_DEBUGGER
#include <utility>
using namespace facebook::hermes;
namespace facebook::react::jsinspector_modern {
namespace {
const uint16_t HERMES_SAMPLING_FREQUENCY_HZ = 10000;
class HermesRuntimeSamplingProfileDelegate {
public:
explicit HermesRuntimeSamplingProfileDelegate(
std::shared_ptr<HermesRuntime> hermesRuntime)
: hermesRuntime_(std::move(hermesRuntime)) {}
void startSampling() {
auto* hermesAPI = jsi::castInterface<IHermesRootAPI>(makeHermesRootAPI());
hermesAPI->enableSamplingProfiler(HERMES_SAMPLING_FREQUENCY_HZ);
}
void stopSampling() {
auto* hermesAPI = jsi::castInterface<IHermesRootAPI>(makeHermesRootAPI());
hermesAPI->disableSamplingProfiler();
}
tracing::RuntimeSamplingProfile collectSamplingProfile() {
return tracing::HermesRuntimeSamplingProfileSerializer::
serializeToTracingSamplingProfile(
hermesRuntime_->dumpSampledTraceToProfile());
}
private:
std::shared_ptr<HermesRuntime> hermesRuntime_;
};
} // namespace
#ifdef HERMES_ENABLE_DEBUGGER
class HermesRuntimeTargetDelegate::Impl final : public RuntimeTargetDelegate {
using HermesStackTrace = debugger::StackTrace;
class HermesStackTraceWrapper : public StackTrace {
public:
explicit HermesStackTraceWrapper(HermesStackTrace&& hermesStackTrace)
: hermesStackTrace_{std::move(hermesStackTrace)} {}
HermesStackTrace& operator*() {
return hermesStackTrace_;
}
HermesStackTrace* operator->() {
return &hermesStackTrace_;
}
const HermesStackTrace& operator*() const {
return hermesStackTrace_;
}
const HermesStackTrace* operator->() const {
return &hermesStackTrace_;
}
private:
HermesStackTrace hermesStackTrace_;
};
public:
explicit Impl(
HermesRuntimeTargetDelegate& delegate,
std::shared_ptr<HermesRuntime> hermesRuntime)
: delegate_(delegate),
runtime_(hermesRuntime),
cdpDebugAPI_(CDPDebugAPI::create(*runtime_)),
samplingProfileDelegate_(
std::make_unique<HermesRuntimeSamplingProfileDelegate>(
std::move(hermesRuntime))) {}
CDPDebugAPI& getCDPDebugAPI() {
return *cdpDebugAPI_;
}
// RuntimeTargetDelegate methods
std::unique_ptr<RuntimeAgentDelegate> createAgentDelegate(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>
previouslyExportedState,
const ExecutionContextDescription& executionContextDescription,
RuntimeExecutor runtimeExecutor) override {
return std::unique_ptr<RuntimeAgentDelegate>(new HermesRuntimeAgentDelegate(
frontendChannel,
sessionState,
std::move(previouslyExportedState),
executionContextDescription,
*runtime_,
delegate_,
std::move(runtimeExecutor)));
}
void addConsoleMessage(jsi::Runtime& /*unused*/, ConsoleMessage message)
override {
using HermesConsoleMessage = facebook::hermes::cdp::ConsoleMessage;
using HermesConsoleAPIType = facebook::hermes::cdp::ConsoleAPIType;
HermesConsoleAPIType type{};
switch (message.type) {
case ConsoleAPIType::kLog:
type = HermesConsoleAPIType::kLog;
break;
case ConsoleAPIType::kDebug:
type = HermesConsoleAPIType::kDebug;
break;
case ConsoleAPIType::kInfo:
type = HermesConsoleAPIType::kInfo;
break;
case ConsoleAPIType::kError:
type = HermesConsoleAPIType::kError;
break;
case ConsoleAPIType::kWarning:
type = HermesConsoleAPIType::kWarning;
break;
case ConsoleAPIType::kDir:
type = HermesConsoleAPIType::kDir;
break;
case ConsoleAPIType::kDirXML:
type = HermesConsoleAPIType::kDirXML;
break;
case ConsoleAPIType::kTable:
type = HermesConsoleAPIType::kTable;
break;
case ConsoleAPIType::kTrace:
type = HermesConsoleAPIType::kTrace;
break;
case ConsoleAPIType::kStartGroup:
type = HermesConsoleAPIType::kStartGroup;
break;
case ConsoleAPIType::kStartGroupCollapsed:
type = HermesConsoleAPIType::kStartGroupCollapsed;
break;
case ConsoleAPIType::kEndGroup:
type = HermesConsoleAPIType::kEndGroup;
break;
case ConsoleAPIType::kClear:
type = HermesConsoleAPIType::kClear;
break;
case ConsoleAPIType::kAssert:
type = HermesConsoleAPIType::kAssert;
break;
case ConsoleAPIType::kTimeEnd:
type = HermesConsoleAPIType::kTimeEnd;
break;
case ConsoleAPIType::kCount:
type = HermesConsoleAPIType::kCount;
break;
default:
throw std::logic_error{"Unknown console message type"};
}
HermesStackTrace hermesStackTrace{};
if (auto hermesStackTraceWrapper =
dynamic_cast<HermesStackTraceWrapper*>(message.stackTrace.get())) {
hermesStackTrace = std::move(**hermesStackTraceWrapper);
}
HermesConsoleMessage hermesConsoleMessage{
message.timestamp,
type,
std::move(message.args),
std::move(hermesStackTrace)};
cdpDebugAPI_->addConsoleMessage(std::move(hermesConsoleMessage));
}
bool supportsConsole() const override {
return true;
}
std::unique_ptr<StackTrace> captureStackTrace(
jsi::Runtime& /* runtime */,
size_t /* framesToSkip */) override {
// TODO(moti): Pass framesToSkip to Hermes. Ignoring framesToSkip happens
// to work for our current use case, because the HostFunction frame we want
// to skip is stripped by CDPDebugAPI::addConsoleMessage before being sent
// to the client. This is still conceptually wrong and could block us from
// properly representing the stack trace in other use cases, where native
// frames aren't stripped on serialisation.
return std::make_unique<HermesStackTraceWrapper>(
runtime_->getDebugger().captureStackTrace());
}
void enableSamplingProfiler() override {
samplingProfileDelegate_->startSampling();
}
void disableSamplingProfiler() override {
samplingProfileDelegate_->stopSampling();
}
tracing::RuntimeSamplingProfile collectSamplingProfile() override {
return samplingProfileDelegate_->collectSamplingProfile();
}
std::optional<folly::dynamic> serializeStackTrace(
const StackTrace& stackTrace) override {
if (auto* hermesStackTraceWrapper =
dynamic_cast<const HermesStackTraceWrapper*>(&stackTrace)) {
// The logic below is duplicated from
// facebook::hermes::cdp::message::makeCallFrames in
// hermes/cdp/MessageConverters.cpp (and rewritten to use Folly).
// TODO: Use a suitable Hermes API (D83560910 / D83560972 / D83562078) to
// serialize the stack trace to CDP-formatted JSON.
folly::dynamic cdpStackTrace = folly::dynamic::object();
auto& hermesStackTrace = **hermesStackTraceWrapper;
if (hermesStackTrace.callFrameCount() > 0) {
folly::dynamic callFrames = folly::dynamic::array();
callFrames.reserve(hermesStackTrace.callFrameCount());
for (int i = 0, n = hermesStackTrace.callFrameCount(); i != n; i++) {
auto callFrame = hermesStackTrace.callFrameForIndex(i);
if (callFrame.location.fileId ==
facebook::hermes::debugger::kInvalidLocation) {
continue;
}
folly::dynamic callFrameObj = folly::dynamic::object();
callFrameObj["functionName"] = callFrame.functionName;
callFrameObj["scriptId"] = std::to_string(callFrame.location.fileId);
callFrameObj["url"] = callFrame.location.fileName;
if (callFrame.location.line !=
facebook::hermes::debugger::kInvalidLocation) {
callFrameObj["lineNumber"] = callFrame.location.line - 1;
}
if (callFrame.location.column !=
facebook::hermes::debugger::kInvalidLocation) {
callFrameObj["columnNumber"] = callFrame.location.column - 1;
}
callFrames.push_back(std::move(callFrameObj));
}
cdpStackTrace["callFrames"] = std::move(callFrames);
}
return cdpStackTrace;
}
return std::nullopt;
}
private:
HermesRuntimeTargetDelegate& delegate_;
std::shared_ptr<HermesRuntime> runtime_;
const std::unique_ptr<CDPDebugAPI> cdpDebugAPI_;
std::unique_ptr<HermesRuntimeSamplingProfileDelegate>
samplingProfileDelegate_;
};
#else
/**
* A stub for HermesRuntimeTargetDelegate when Hermes is compiled without
* debugging support.
*/
class HermesRuntimeTargetDelegate::Impl final
: public FallbackRuntimeTargetDelegate {
public:
explicit Impl(
HermesRuntimeTargetDelegate&,
std::shared_ptr<HermesRuntime> hermesRuntime)
: FallbackRuntimeTargetDelegate{hermesRuntime->description()},
samplingProfileDelegate_(
std::make_unique<HermesRuntimeSamplingProfileDelegate>(
std::move(hermesRuntime))) {}
void enableSamplingProfiler() override {
samplingProfileDelegate_->startSampling();
}
void disableSamplingProfiler() override {
samplingProfileDelegate_->stopSampling();
}
tracing::RuntimeSamplingProfile collectSamplingProfile() override {
return samplingProfileDelegate_->collectSamplingProfile();
}
private:
std::unique_ptr<HermesRuntimeSamplingProfileDelegate>
samplingProfileDelegate_;
};
#endif // HERMES_ENABLE_DEBUGGER
HermesRuntimeTargetDelegate::HermesRuntimeTargetDelegate(
std::shared_ptr<HermesRuntime> hermesRuntime)
: impl_(std::make_unique<Impl>(*this, std::move(hermesRuntime))) {}
HermesRuntimeTargetDelegate::~HermesRuntimeTargetDelegate() = default;
std::unique_ptr<RuntimeAgentDelegate>
HermesRuntimeTargetDelegate::createAgentDelegate(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>
previouslyExportedState,
const ExecutionContextDescription& executionContextDescription,
RuntimeExecutor runtimeExecutor) {
return impl_->createAgentDelegate(
frontendChannel,
sessionState,
std::move(previouslyExportedState),
executionContextDescription,
std::move(runtimeExecutor));
}
void HermesRuntimeTargetDelegate::addConsoleMessage(
jsi::Runtime& runtime,
ConsoleMessage message) {
impl_->addConsoleMessage(runtime, std::move(message));
}
bool HermesRuntimeTargetDelegate::supportsConsole() const {
return impl_->supportsConsole();
}
std::unique_ptr<StackTrace> HermesRuntimeTargetDelegate::captureStackTrace(
jsi::Runtime& runtime,
size_t framesToSkip) {
return impl_->captureStackTrace(runtime, framesToSkip);
}
void HermesRuntimeTargetDelegate::enableSamplingProfiler() {
impl_->enableSamplingProfiler();
}
void HermesRuntimeTargetDelegate::disableSamplingProfiler() {
impl_->disableSamplingProfiler();
}
tracing::RuntimeSamplingProfile
HermesRuntimeTargetDelegate::collectSamplingProfile() {
return impl_->collectSamplingProfile();
}
std::optional<folly::dynamic> HermesRuntimeTargetDelegate::serializeStackTrace(
const StackTrace& stackTrace) {
return impl_->serializeStackTrace(stackTrace);
}
#ifdef HERMES_ENABLE_DEBUGGER
CDPDebugAPI& HermesRuntimeTargetDelegate::getCDPDebugAPI() {
return impl_->getCDPDebugAPI();
}
#endif
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,75 @@
/*
* 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.
*/
#pragma once
#include <ReactCommon/RuntimeExecutor.h>
#include <cxxreact/MessageQueueThread.h>
#include <hermes/hermes.h>
#include <jsinspector-modern/ReactCdp.h>
#ifdef HERMES_ENABLE_DEBUGGER
#include <hermes/cdp/CDPDebugAPI.h>
#endif
#include <memory>
namespace facebook::react::jsinspector_modern {
/**
* A RuntimeTargetDelegate that enables debugging a Hermes runtime over CDP.
*/
class HermesRuntimeTargetDelegate : public RuntimeTargetDelegate {
public:
/**
* Creates a HermesRuntimeTargetDelegate for the given runtime.
*/
explicit HermesRuntimeTargetDelegate(std::shared_ptr<hermes::HermesRuntime> hermesRuntime);
~HermesRuntimeTargetDelegate() override;
// RuntimeTargetDelegate methods
std::unique_ptr<jsinspector_modern::RuntimeAgentDelegate> createAgentDelegate(
jsinspector_modern::FrontendChannel frontendChannel,
jsinspector_modern::SessionState &sessionState,
std::unique_ptr<jsinspector_modern::RuntimeAgentDelegate::ExportedState> previouslyExportedState,
const jsinspector_modern::ExecutionContextDescription &executionContextDescription,
RuntimeExecutor runtimeExecutor) override;
void addConsoleMessage(jsi::Runtime &runtime, ConsoleMessage message) override;
bool supportsConsole() const override;
std::unique_ptr<StackTrace> captureStackTrace(jsi::Runtime &runtime, size_t framesToSkip) override;
void enableSamplingProfiler() override;
void disableSamplingProfiler() override;
tracing::RuntimeSamplingProfile collectSamplingProfile() override;
std::optional<folly::dynamic> serializeStackTrace(const StackTrace &stackTrace) override;
private:
// We use the private implementation idiom to ensure this class has the same
// layout regardless of whether HERMES_ENABLE_DEBUGGER is defined. The net
// effect is that callers can include HermesRuntimeTargetDelegate.h without
// setting HERMES_ENABLE_DEBUGGER one way or the other.
class Impl;
// Callers within this library may set HERMES_ENABLE_DEBUGGER to see this extra
// API.
#ifdef HERMES_ENABLE_DEBUGGER
friend class HermesRuntimeAgentDelegate;
hermes::cdp::CDPDebugAPI &getCDPDebugAPI();
#endif
std::unique_ptr<Impl> impl_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,37 @@
/*
* 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 "Registration.h"
#include "ConnectionDemux.h"
#if defined(HERMES_ENABLE_DEBUGGER) && !defined(HERMES_V1_ENABLED)
namespace facebook::hermes::inspector_modern::chrome {
namespace {
ConnectionDemux& demux() {
static ConnectionDemux instance{
facebook::react::jsinspector_modern::getInspectorInstance()};
return instance;
}
} // namespace
DebugSessionToken enableDebugging(
std::unique_ptr<RuntimeAdapter> adapter,
const std::string& title) {
return demux().enableDebugging(std::move(adapter), title);
}
void disableDebugging(DebugSessionToken session) {
demux().disableDebugging(session);
}
} // namespace facebook::hermes::inspector_modern::chrome
#endif // defined(HERMES_ENABLE_DEBUGGER) && !defined(HERMES_V1_ENABLED)

View File

@@ -0,0 +1,39 @@
/*
* 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.
*/
#pragma once
#if defined(HERMES_ENABLE_DEBUGGER) && !defined(HERMES_V1_ENABLED)
#include <memory>
#include <string>
#include <hermes/hermes.h>
#include <hermes/inspector/RuntimeAdapter.h>
namespace facebook::hermes::inspector_modern::chrome {
using DebugSessionToken = int;
/*
* enableDebugging adds this runtime to the list of debuggable JS targets
* (called "pages" in the higher-level React Native API) in this process. It
* should be called before any JS runs in the runtime. The returned token
* can be used to disable debugging for this runtime.
*/
extern DebugSessionToken enableDebugging(std::unique_ptr<RuntimeAdapter> adapter, const std::string &title);
/*
* disableDebugging removes this runtime from the list of debuggable JS targets
* in this process. The runtime to remove is identified by the token returned
* from enableDebugging.
*/
extern void disableDebugging(DebugSessionToken session);
} // namespace facebook::hermes::inspector_modern::chrome
#endif // defined(HERMES_ENABLE_DEBUGGER) && !defined(HERMES_V1_ENABLED)

View File

@@ -0,0 +1,148 @@
/*
* 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 <chrono>
#include <condition_variable>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
#include <gtest/gtest.h>
#include <hermes/hermes.h>
#include <hermes/inspector-modern/chrome/ConnectionDemux.h>
#include <jsinspector-modern/InspectorInterfaces.h>
namespace facebook {
namespace hermes {
namespace inspector_modern {
namespace chrome {
using ::facebook::react::jsinspector_modern::IInspector;
using ::facebook::react::jsinspector_modern::InspectorPageDescription;
using ::facebook::react::jsinspector_modern::IRemoteConnection;
namespace {
std::unordered_map<int, std::string> makePageMap(
const std::vector<InspectorPageDescription>& pages) {
std::unordered_map<int, std::string> pageMap;
for (auto& page : pages) {
pageMap[page.id] = page.title;
}
return pageMap;
}
void expectPages(
IInspector& inspector,
const std::unordered_map<int, std::string>& expected) {
auto pages = makePageMap(inspector.getPages());
EXPECT_EQ(pages, expected);
}
class TestRemoteConnection : public IRemoteConnection {
public:
class Data {
public:
void expectDisconnected() {
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait_for(
lock, std::chrono::milliseconds(2500), [&] { return !connected_; });
EXPECT_FALSE(connected_);
}
void setDisconnected() {
std::scoped_lock lock(mutex_);
connected_ = false;
cv_.notify_one();
}
private:
std::mutex mutex_;
std::condition_variable cv_;
bool connected_{true};
};
TestRemoteConnection() : data_(std::make_shared<Data>()) {}
~TestRemoteConnection() {}
void onMessage(std::string message) override {}
void onDisconnect() override {
data_->setDisconnected();
}
std::shared_ptr<Data> getData() {
return data_;
}
private:
std::shared_ptr<Data> data_;
};
}; // namespace
TEST(ConnectionDemuxTests, TestEnableDisable) {
std::shared_ptr<HermesRuntime> runtime1(
facebook::hermes::makeHermesRuntime());
std::shared_ptr<HermesRuntime> runtime2(
facebook::hermes::makeHermesRuntime());
auto inspector =
facebook::react::jsinspector_modern::makeTestInspectorInstance();
ConnectionDemux demux{*inspector};
int id1 = demux.enableDebugging(
std::make_unique<SharedRuntimeAdapter>(runtime1), "page1");
int id2 = demux.enableDebugging(
std::make_unique<SharedRuntimeAdapter>(runtime2), "page2");
expectPages(*inspector, {{id1, "page1"}, {id2, "page2"}});
auto remoteConn1 = std::make_unique<TestRemoteConnection>();
auto remoteData1 = remoteConn1->getData();
auto localConn1 = inspector->connect(id1, std::move(remoteConn1));
EXPECT_NE(localConn1.get(), nullptr);
{
// If we connect to the same page id again without disconnecting, we should
// get null
auto remoteConn = std::make_unique<TestRemoteConnection>();
auto localConn = inspector->connect(id1, std::move(remoteConn));
EXPECT_EQ(localConn.get(), nullptr);
}
auto remoteConn2 = std::make_unique<TestRemoteConnection>();
auto remoteData2 = remoteConn2->getData();
auto localConn2 = inspector->connect(id2, std::move(remoteConn2));
EXPECT_NE(localConn2.get(), nullptr);
// Disable debugging on runtime2. This should remove its page from the list
// and call onDisconnect on its remoteConn
demux.disableDebugging(id2);
expectPages(*inspector, {{id1, "page1"}});
remoteData2->expectDisconnected();
// Disconnect conn1. Its page should still be in the page list and
// onDisconnect should be called.
localConn1->disconnect();
remoteData1->expectDisconnected();
{
// Should still be able to reconnect after disconnecting
auto remoteConn = std::make_unique<TestRemoteConnection>();
auto localConn = inspector->connect(id1, std::move(remoteConn));
EXPECT_NE(localConn.get(), nullptr);
}
}
} // namespace chrome
} // namespace inspector_modern
} // namespace hermes
} // namespace facebook