Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class HermesRuntimeSamplingProfileDelegate {
} // namespace

#ifdef HERMES_ENABLE_DEBUGGER

class HermesRuntimeTargetDelegate::Impl final : public RuntimeTargetDelegate {
using HermesStackTrace = debugger::StackTrace;

Expand All @@ -78,6 +79,14 @@ class HermesRuntimeTargetDelegate::Impl final : public RuntimeTargetDelegate {
return &hermesStackTrace_;
}

const HermesStackTrace& operator*() const {
return hermesStackTrace_;
}

const HermesStackTrace* operator->() const {
return &hermesStackTrace_;
}

private:
HermesStackTrace hermesStackTrace_;
};
Expand Down Expand Up @@ -216,6 +225,16 @@ class HermesRuntimeTargetDelegate::Impl final : public RuntimeTargetDelegate {
return samplingProfileDelegate_->collectSamplingProfile();
}

std::optional<folly::dynamic> serializeStackTrace(
const StackTrace& stackTrace) override {
if (auto* hermesStackTraceWrapper =
dynamic_cast<const HermesStackTraceWrapper*>(&stackTrace)) {
return folly::parseJson(cdpDebugAPI_->serializeStackTraceToJsonStr(
**hermesStackTraceWrapper));
}
return std::nullopt;
}

private:
HermesRuntimeTargetDelegate& delegate_;
std::shared_ptr<HermesRuntime> runtime_;
Expand Down Expand Up @@ -311,6 +330,11 @@ 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ class HermesRuntimeTargetDelegate : public RuntimeTargetDelegate {

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ target_merge_so(jsinspector)
target_include_directories(jsinspector PUBLIC ${REACT_COMMON_DIR})

target_link_libraries(jsinspector
boost
folly_runtime
glog
jsinspector_network
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,10 @@ FallbackRuntimeTargetDelegate::collectSamplingProfile() {
"Sampling Profiler capabilities are not supported for Runtime fallback");
}

std::optional<folly::dynamic>
FallbackRuntimeTargetDelegate::serializeStackTrace(
const StackTrace& stackTrace) {
return std::nullopt;
}

} // namespace facebook::react::jsinspector_modern
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ class FallbackRuntimeTargetDelegate : public RuntimeTargetDelegate {

tracing::RuntimeSamplingProfile collectSamplingProfile() override;

std::optional<folly::dynamic> serializeStackTrace(
const StackTrace& stackTrace) override;

private:
std::string engineDescription_;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Pod::Spec.new do |s|

resolve_use_frameworks(s, module_name: module_name)

add_dependency(s, "boost")
add_dependency(s, "React-oscompat") # Needed for USE_FRAMEWORKS=dynamic
s.dependency "React-featureflags"
add_dependency(s, "React-runtimeexecutor", :additional_framework_paths => ["platform/ios"])
Expand All @@ -55,7 +56,6 @@ Pod::Spec.new do |s|
add_dependency(s, "React-jsinspectortracing", :framework_name => 'jsinspector_moderntracing')
s.dependency "React-perflogger", version
add_dependency(s, "React-oscompat")

if use_hermes()
s.dependency "hermes-engine"
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@ RuntimeTarget::RuntimeTarget(
void RuntimeTarget::installGlobals() {
// NOTE: RuntimeTarget::installConsoleHandler is in RuntimeTargetConsole.cpp
installConsoleHandler();
// NOTE: RuntimeTarget::installDebuggerSessionObserver is in
// RuntimeTargetDebuggerSessionObserver.cpp
installDebuggerSessionObserver();
// NOTE: RuntimeTarget::installNetworkReporterAPI is in
// RuntimeTargetNetwork.cpp
installNetworkReporterAPI();
}

std::shared_ptr<RuntimeAgent> RuntimeTarget::createAgent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ class RuntimeTargetDelegate {
* Return recorded sampling profile for the previous sampling session.
*/
virtual tracing::RuntimeSamplingProfile collectSamplingProfile() = 0;

/**
* \returns a JSON representation of the given stack trace, conforming to the
* @cdp Runtime.StackTrace type, if the runtime supports it. Otherwise,
* returns std::nullopt.
*/
virtual std::optional<folly::dynamic> serializeStackTrace(
const StackTrace& stackTrace) = 0;
};

/**
Expand Down Expand Up @@ -291,6 +299,12 @@ class JSINSPECTOR_EXPORT RuntimeTarget
*/
void installDebuggerSessionObserver();

/**
* Installs the private __NETWORK_REPORTER__ object on the Runtime's
* global object.
*/
void installNetworkReporterAPI();

/**
* Propagates the debugger session state change to the JavaScript via calling
* onStatusChange on __DEBUGGER_SESSION_OBSERVER__.
Expand All @@ -303,6 +317,50 @@ class JSINSPECTOR_EXPORT RuntimeTarget
*/
void emitDebuggerSessionDestroyed();

/**
* Run the callback \p func synchronously while on the JS thread and pass it a
* safe reference to our RuntimeTargetDelegate for use on the JS thread. \see
* RuntimeTargetDelegate for information on which methods are safe to call on
* the JS thread. \warning The callback will not run if the RuntimeTarget has
* been destroyed.
* The \p runtime parameter is required to signify that the function is being
* called on the JS thread.
*/
template <std::invocable<RuntimeTarget&> Fn>
static void tryRunWithSelfSync(
std::weak_ptr<RuntimeTarget> selfWeak,
jsi::Runtime& /*unused*/,
Fn func) {
if (auto self = selfWeak.lock()) {
auto selfExecutor = self->executorFromThis();
// Q: Why is it safe to use self->delegate_ here?
// A: Because the caller of InspectorTarget::registerRuntime
// is explicitly required to guarantee that the delegate not
// only outlives the target, but also outlives all JS code
// execution that occurs on the JS thread.
func(*self);
// To ensure we never destroy `self` on the JS thread, send
// our shared_ptr back to the inspector thread.
selfExecutor([self = std::move(self)](auto&) { (void)self; });
}
}

/**
* \returns an opaque representation of the current stack trace if the
* RuntimeTarget is valid, or std::nullopt otherwise.
* \see RuntimeTargetDelegate::captureStackTrace
*/
static std::optional<std::unique_ptr<StackTrace>> tryCaptureStackTrace(
std::weak_ptr<RuntimeTarget> selfWeak,
jsi::Runtime& runtime,
size_t framesToSkip = 0);

/**
* \returns a globally unique ID for a network request.
* May be called from any thread as long as the RuntimeTarget is valid.
*/
std::string createNetworkRequestId();

// Necessary to allow RuntimeAgent to access RuntimeTarget's internals in a
// controlled way (i.e. only RuntimeTargetController gets friend access, while
// RuntimeAgent itself doesn't).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* 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 <jsinspector-modern/RuntimeTarget.h>
#include <jsinspector-modern/network/NetworkHandler.h>

#include <boost/uuid/random_generator.hpp>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_io.hpp>

using namespace facebook::jsi;
using namespace std::string_literals;

namespace facebook::react::jsinspector_modern {

namespace {

/**
* JS `Object.create()`
*/
jsi::Object objectCreate(jsi::Runtime& runtime, jsi::Value prototype) {
auto objectGlobal = runtime.global().getPropertyAsObject(runtime, "Object");
auto createFn = objectGlobal.getPropertyAsFunction(runtime, "create");
return createFn.callWithThis(runtime, objectGlobal, prototype)
.getObject(runtime);
}

/**
* JS `Object.freeze
*/
jsi::Object objectFreeze(jsi::Runtime& runtime, jsi::Object object) {
auto objectGlobal = runtime.global().getPropertyAsObject(runtime, "Object");
auto freezeFn = objectGlobal.getPropertyAsFunction(runtime, "freeze");
return freezeFn.callWithThis(runtime, objectGlobal, object)
.getObject(runtime);
}

} // namespace

void RuntimeTarget::installNetworkReporterAPI() {
jsExecutor_([selfWeak = weak_from_this(),
selfExecutor = executorFromThis()](jsi::Runtime& runtime) {
auto globalObj = runtime.global();
auto networkReporterApi = objectCreate(runtime, nullptr);
networkReporterApi.setProperty(
runtime,
"createDevToolsRequestId",
Function::createFromHostFunction(
runtime,
PropNameID::forAscii(runtime, "createDevToolsRequestId"),
0,
[selfWeak](
Runtime& rt,
const Value& thisVal,
const Value* args,
size_t count) -> Value {
std::optional<std::string> devToolsRequestId;
RuntimeTarget::tryRunWithSelfSync(
selfWeak, rt, [&](RuntimeTarget& self) {
devToolsRequestId = self.createNetworkRequestId();
// Q: Why is it safe to use self.delegate_ here?
// A: Because the caller of InspectorTarget::registerRuntime
// is explicitly required to guarantee that the delegate not
// only outlives the target, but also outlives all JS code
// execution that occurs on the JS thread.
auto stackTrace = self.delegate_.captureStackTrace(rt);
// TODO(moti): Instead of checking the singleton state,
// directly check whether the current target has a session
// with the Network domain enabled.
if (NetworkHandler::getInstance().isEnabled()) {
auto cdpStackTrace =
self.delegate_.serializeStackTrace(*stackTrace);
if (cdpStackTrace) {
NetworkHandler::getInstance()
.recordRequestInitiatorStack(
*devToolsRequestId, std::move(*cdpStackTrace));
}
}
});
if (!devToolsRequestId) {
throw JSError(rt, "React Native Runtime is shutting down");
}
return String::createFromUtf8(rt, *devToolsRequestId);
}));
networkReporterApi = objectFreeze(runtime, std::move(networkReporterApi));
globalObj.setProperty(runtime, "__NETWORK_REPORTER__", networkReporterApi);
});
}

std::string RuntimeTarget::createNetworkRequestId() {
return boost::uuids::to_string(boost::uuids::random_generator()());
}

} // namespace facebook::react::jsinspector_modern
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ void NetworkHandler::onRequestWillBeSent(
}

double timestamp = getCurrentUnixTimestampSeconds();
std::optional<folly::dynamic> initiator;
initiator = consumeStoredRequestInitiator(requestId);
auto params = cdp::network::RequestWillBeSentParams{
.requestId = requestId,
.loaderId = "",
Expand All @@ -79,7 +81,9 @@ void NetworkHandler::onRequestWillBeSent(
// Unix epoch for both.
.timestamp = timestamp,
.wallTime = timestamp,
.initiator = folly::dynamic::object("type", "script"),
.initiator = initiator.has_value()
? std::move(initiator.value())
: folly::dynamic::object("type", "script"),
.redirectHasExtraInfo = redirectResponse.has_value(),
.redirectResponse = redirectResponse,
};
Expand Down Expand Up @@ -114,7 +118,7 @@ void NetworkHandler::onResponseReceived(

auto resourceType = cdp::network::resourceTypeFromMimeType(response.mimeType);
{
std::lock_guard<std::mutex> lock(resourceTypeMapMutex_);
std::lock_guard<std::mutex> lock(requestMetadataMutex_);
resourceTypeMap_.emplace(requestId, resourceType);
}

Expand Down Expand Up @@ -175,7 +179,7 @@ void NetworkHandler::onLoadingFailed(
}

{
std::lock_guard<std::mutex> lock(resourceTypeMapMutex_);
std::lock_guard<std::mutex> lock(requestMetadataMutex_);
auto params = cdp::network::LoadingFailedParams{
.requestId = requestId,
.timestamp = getCurrentUnixTimestampSeconds(),
Expand Down Expand Up @@ -212,4 +216,30 @@ std::optional<std::tuple<std::string, bool>> NetworkHandler::getResponseBody(
responseBody->data, responseBody->base64Encoded);
}

void NetworkHandler::recordRequestInitiatorStack(
const std::string& requestId,
folly::dynamic stackTrace) {
if (!isEnabledNoSync()) {
return;
}

std::lock_guard<std::mutex> lock(requestMetadataMutex_);
requestInitiatorById_.emplace(
requestId,
folly::dynamic::object("type", "script")("stack", std::move(stackTrace)));
}

std::optional<folly::dynamic> NetworkHandler::consumeStoredRequestInitiator(
const std::string& requestId) {
std::lock_guard<std::mutex> lock(requestMetadataMutex_);
auto it = requestInitiatorById_.find(requestId);
if (it == requestInitiatorById_.end()) {
return std::nullopt;
}
// Remove and return
auto result = std::move(it->second);
requestInitiatorById_.erase(it);
return result;
}

} // namespace facebook::react::jsinspector_modern
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ class NetworkHandler {
std::optional<std::tuple<std::string, bool>> getResponseBody(
const std::string& requestId);

/**
* Associate the given stack trace with the given request ID.
*/
void recordRequestInitiatorStack(
const std::string& requestId,
folly::dynamic stackTrace);

private:
NetworkHandler() = default;
NetworkHandler(const NetworkHandler&) = delete;
Expand All @@ -136,10 +143,14 @@ class NetworkHandler {
return enabled_.load(std::memory_order_relaxed);
}

std::optional<folly::dynamic> consumeStoredRequestInitiator(
const std::string& requestId);

FrontendChannel frontendChannel_;

std::map<std::string, std::string> resourceTypeMap_{};
std::mutex resourceTypeMapMutex_{};
std::map<std::string, folly::dynamic> requestInitiatorById_{};
std::mutex requestMetadataMutex_{};

BoundedRequestBuffer responseBodyBuffer_{};
std::mutex requestBodyMutex_;
Expand Down
Loading
Loading