mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Summary: Reland of https://github.com/facebook/react-native/issues/32895 with fix for optional params object. Original description: I was looking at the hermes chrome devtools integration and noticed requests to `Runtime.getHeapUsage` which was not implemented. When implemented it will show a summary of memory usage of the javascript instance in devtools. <img width="325" alt="image" src="https://user-images.githubusercontent.com/2677334/149637113-e1d95d26-9e26-46c2-9be6-47d22284f15f.png"> ## Changelog [General] [Added] - Implement Runtime.getHeapUsage for hermes chrome inspector Pull Request resolved: https://github.com/facebook/react-native/pull/33173 Test Plan: I was able to reproduce the issue that caused the initial PR to be reverted using the resume request in Flipper. Pausing JS execution then resuming it will cause it to crash before this change, and works fine after. Before <img width="912" alt="image" src="https://user-images.githubusercontent.com/2677334/149637073-15f4e1fa-8183-42dc-8673-d4371731415c.png"> After <img width="1076" alt="image" src="https://user-images.githubusercontent.com/2677334/149637085-579dee8f-5efb-4658-b0a8-2400bd119924.png"> Reviewed By: jpporto Differential Revision: D34446672 Pulled By: ShikaSD fbshipit-source-id: 6e26b8d53cd88cddded36437c72a01822551b9d0
1505 lines
53 KiB
C++
1505 lines
53 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 "Connection.h"
|
|
|
|
#include <cstdlib>
|
|
#include <mutex>
|
|
#include <sstream>
|
|
|
|
#include <folly/Conv.h>
|
|
#include <folly/Executor.h>
|
|
#include <folly/Function.h>
|
|
#include <folly/json.h>
|
|
#include <glog/logging.h>
|
|
#include <hermes/inspector/Inspector.h>
|
|
#include <hermes/inspector/chrome/MessageConverters.h>
|
|
#include <hermes/inspector/chrome/RemoteObjectsTable.h>
|
|
#include <hermes/inspector/detail/CallbackOStream.h>
|
|
#include <hermes/inspector/detail/SerialExecutor.h>
|
|
#include <hermes/inspector/detail/Thread.h>
|
|
#include <jsi/instrumentation.h>
|
|
|
|
namespace facebook {
|
|
namespace hermes {
|
|
namespace inspector {
|
|
namespace chrome {
|
|
|
|
using ::facebook::react::ILocalConnection;
|
|
using ::facebook::react::IRemoteConnection;
|
|
using ::folly::Unit;
|
|
|
|
namespace debugger = ::facebook::hermes::debugger;
|
|
namespace inspector = ::facebook::hermes::inspector;
|
|
namespace m = ::facebook::hermes::inspector::chrome::message;
|
|
|
|
static const char *const kVirtualBreakpointPrefix = "virtualbreakpoint-";
|
|
static const char *const kBeforeScriptWithSourceMapExecution =
|
|
"beforeScriptWithSourceMapExecution";
|
|
|
|
/*
|
|
* Connection::Impl
|
|
*/
|
|
|
|
class Connection::Impl : public inspector::InspectorObserver,
|
|
public message::RequestHandler {
|
|
public:
|
|
Impl(
|
|
std::unique_ptr<RuntimeAdapter> adapter,
|
|
const std::string &title,
|
|
bool waitForDebugger);
|
|
~Impl();
|
|
|
|
jsi::Runtime &getRuntime();
|
|
std::string getTitle() const;
|
|
|
|
bool connect(std::unique_ptr<IRemoteConnection> remoteConn);
|
|
bool disconnect();
|
|
void sendMessage(std::string str);
|
|
|
|
/* InspectorObserver overrides */
|
|
void onBreakpointResolved(
|
|
Inspector &inspector,
|
|
const debugger::BreakpointInfo &info) override;
|
|
void onContextCreated(Inspector &inspector) override;
|
|
void onPause(Inspector &inspector, const debugger::ProgramState &state)
|
|
override;
|
|
void onResume(Inspector &inspector) override;
|
|
void onScriptParsed(Inspector &inspector, const ScriptInfo &info) override;
|
|
void onMessageAdded(Inspector &inspector, const ConsoleMessageInfo &info)
|
|
override;
|
|
|
|
/* RequestHandler overrides */
|
|
void handle(const m::UnknownRequest &req) override;
|
|
void handle(const m::debugger::DisableRequest &req) override;
|
|
void handle(const m::debugger::EnableRequest &req) override;
|
|
void handle(const m::debugger::EvaluateOnCallFrameRequest &req) override;
|
|
void handle(const m::debugger::PauseRequest &req) override;
|
|
void handle(const m::debugger::RemoveBreakpointRequest &req) override;
|
|
void handle(const m::debugger::ResumeRequest &req) override;
|
|
void handle(const m::debugger::SetBreakpointRequest &req) override;
|
|
void handle(const m::debugger::SetBreakpointByUrlRequest &req) override;
|
|
void handle(const m::debugger::SetBreakpointsActiveRequest &req) override;
|
|
void handle(
|
|
const m::debugger::SetInstrumentationBreakpointRequest &req) override;
|
|
void handle(const m::debugger::SetPauseOnExceptionsRequest &req) override;
|
|
void handle(const m::debugger::StepIntoRequest &req) override;
|
|
void handle(const m::debugger::StepOutRequest &req) override;
|
|
void handle(const m::debugger::StepOverRequest &req) override;
|
|
void handle(const m::heapProfiler::TakeHeapSnapshotRequest &req) override;
|
|
void handle(
|
|
const m::heapProfiler::StartTrackingHeapObjectsRequest &req) override;
|
|
void handle(
|
|
const m::heapProfiler::StopTrackingHeapObjectsRequest &req) override;
|
|
void handle(const m::heapProfiler::StartSamplingRequest &req) override;
|
|
void handle(const m::heapProfiler::StopSamplingRequest &req) override;
|
|
void handle(const m::heapProfiler::CollectGarbageRequest &req) override;
|
|
void handle(
|
|
const m::heapProfiler::GetObjectByHeapObjectIdRequest &req) override;
|
|
void handle(const m::heapProfiler::GetHeapObjectIdRequest &req) override;
|
|
void handle(const m::runtime::CallFunctionOnRequest &req) override;
|
|
void handle(const m::runtime::EvaluateRequest &req) override;
|
|
void handle(const m::runtime::GetHeapUsageRequest &req) override;
|
|
void handle(const m::runtime::GetPropertiesRequest &req) override;
|
|
void handle(const m::runtime::RunIfWaitingForDebuggerRequest &req) override;
|
|
|
|
private:
|
|
// The execution context id reported back by the ExecutionContextCreated
|
|
// notification. We only ever expect this execution context id.
|
|
static constexpr int32_t kHermesExecutionContextId = 1;
|
|
std::vector<m::runtime::PropertyDescriptor> makePropsFromScope(
|
|
std::pair<uint32_t, uint32_t> frameAndScopeIndex,
|
|
const std::string &objectGroup,
|
|
const debugger::ProgramState &state);
|
|
std::vector<m::runtime::PropertyDescriptor> makePropsFromValue(
|
|
const jsi::Value &value,
|
|
const std::string &objectGroup,
|
|
bool onlyOwnProperties);
|
|
|
|
void sendSnapshot(
|
|
int reqId,
|
|
std::string message,
|
|
bool reportProgress,
|
|
bool stopStackTraceCapture);
|
|
void sendToClient(const std::string &str);
|
|
void sendResponseToClient(const m::Response &resp);
|
|
void sendNotificationToClient(const m::Notification &resp);
|
|
folly::Function<void(const std::exception &)> sendErrorToClient(int id);
|
|
void sendResponseToClientViaExecutor(int id);
|
|
void sendResponseToClientViaExecutor(folly::Future<Unit> future, int id);
|
|
void sendResponseToClientViaExecutor(const m::Response &resp);
|
|
void sendNotificationToClientViaExecutor(const m::Notification ¬e);
|
|
void sendErrorToClientViaExecutor(int id, const std::string &error);
|
|
|
|
std::shared_ptr<RuntimeAdapter> runtimeAdapter_;
|
|
std::string title_;
|
|
|
|
// connected_ is protected by connectionMutex_.
|
|
std::mutex connectionMutex_;
|
|
bool connected_;
|
|
|
|
// parsedScripts_ list stores file names of all scripts that have been
|
|
// parsed so that we could find script's file name by regex.
|
|
// This is similar to Inspector's loadedScripts_ map but we want to
|
|
// store this info here because searching file name that matches
|
|
// given regex (on setBreakpointByUrl command) is more related to Chrome
|
|
// protocol than to Hermes inspector.
|
|
// Access is protected by parsedScriptsMutex_.
|
|
std::mutex parsedScriptsMutex_;
|
|
std::vector<std::string> parsedScripts_;
|
|
|
|
// Some events are represented as a mode in Hermes but a breakpoint in CDP,
|
|
// e.g. "beforeScriptExecution" and "beforeScriptWithSourceMapExecution".
|
|
// Keep track of these separately. The caller should lock the
|
|
// virtualBreakpointMutex_.
|
|
std::mutex virtualBreakpointMutex_;
|
|
uint32_t nextVirtualBreakpoint_ = 1;
|
|
const std::string &createVirtualBreakpoint(const std::string &category);
|
|
bool isVirtualBreakpointId(const std::string &id);
|
|
bool hasVirtualBreakpoint(const std::string &category);
|
|
bool removeVirtualBreakpoint(const std::string &id);
|
|
std::unordered_map<std::string, std::unordered_set<std::string>>
|
|
virtualBreakpoints_;
|
|
|
|
// The rest of these member variables are only accessed via executor_.
|
|
std::unique_ptr<folly::Executor> executor_;
|
|
std::unique_ptr<IRemoteConnection> remoteConn_;
|
|
std::shared_ptr<inspector::Inspector> inspector_;
|
|
|
|
// objTable_ is protected by the inspector lock. It should only be accessed
|
|
// when the VM is paused, e.g. in an InspectorObserver callback or in an
|
|
// executeIfEnabled callback.
|
|
RemoteObjectsTable objTable_;
|
|
};
|
|
|
|
Connection::Impl::Impl(
|
|
std::unique_ptr<RuntimeAdapter> adapter,
|
|
const std::string &title,
|
|
bool waitForDebugger)
|
|
: runtimeAdapter_(std::move(adapter)),
|
|
title_(title),
|
|
connected_(false),
|
|
executor_(std::make_unique<inspector::detail::SerialExecutor>(
|
|
"hermes-chrome-inspector-conn")),
|
|
remoteConn_(nullptr),
|
|
inspector_(std::make_shared<inspector::Inspector>(
|
|
runtimeAdapter_,
|
|
*this,
|
|
waitForDebugger)) {
|
|
inspector_->installLogHandler();
|
|
}
|
|
|
|
Connection::Impl::~Impl() = default;
|
|
|
|
jsi::Runtime &Connection::Impl::getRuntime() {
|
|
return runtimeAdapter_->getRuntime();
|
|
}
|
|
|
|
std::string Connection::Impl::getTitle() const {
|
|
return title_;
|
|
}
|
|
|
|
bool Connection::Impl::connect(std::unique_ptr<IRemoteConnection> remoteConn) {
|
|
assert(remoteConn);
|
|
std::lock_guard<std::mutex> lock(connectionMutex_);
|
|
|
|
if (connected_) {
|
|
return false;
|
|
}
|
|
|
|
connected_ = true;
|
|
executor_->add([this, remoteConn = std::move(remoteConn)]() mutable {
|
|
remoteConn_ = std::move(remoteConn);
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Connection::Impl::disconnect() {
|
|
std::lock_guard<std::mutex> lock(connectionMutex_);
|
|
|
|
if (!connected_) {
|
|
return false;
|
|
}
|
|
|
|
connected_ = false;
|
|
|
|
inspector_->disable().via(executor_.get()).thenValue([this](auto &&) {
|
|
// HACK: We purposely call RemoteConnection::onDisconnect on a *different*
|
|
// thread, rather than on this thread (the executor thread). This is to
|
|
// prevent this scenario:
|
|
//
|
|
// 1. RemoteConnection::onDisconnect runs on the executor thread
|
|
// 2. onDisconnect through a long chain of calls causes the Connection
|
|
// destructor to run
|
|
// 3. The Connection destructor causes the SerialExecutor destructor to run.
|
|
// 4. The SerialExecutor destructor waits for all outstanding work items to
|
|
// finish via a call to join().
|
|
// 5. join() fails, since the executor thread is trying to join against
|
|
// itself.
|
|
//
|
|
// To prevent this chain of events, we always call onDisconnect on a
|
|
// different thread.
|
|
//
|
|
// See P59135203 for an example stack trace.
|
|
//
|
|
// One more hack: we use release() and delete instead of unique_ptr because
|
|
// detail::Thread expects a std::function, and std::function cannot capture
|
|
// move-only types like unique_ptr.
|
|
auto conn = remoteConn_.release();
|
|
inspector::detail::Thread disconnectLaterThread{
|
|
"hermes-chrome-inspector-conn-disconnect", [conn] {
|
|
conn->onDisconnect();
|
|
delete conn;
|
|
}};
|
|
disconnectLaterThread.detach();
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
void Connection::Impl::sendMessage(std::string str) {
|
|
executor_->add([this, str = std::move(str)]() mutable {
|
|
folly::Try<std::unique_ptr<m::Request>> maybeReq =
|
|
m::Request::fromJson(str);
|
|
|
|
if (maybeReq.hasException()) {
|
|
LOG(ERROR) << "Invalid request `" << str
|
|
<< "`: " << maybeReq.exception().what();
|
|
return;
|
|
}
|
|
|
|
auto &req = maybeReq.value();
|
|
if (req) {
|
|
req->accept(*this);
|
|
}
|
|
});
|
|
}
|
|
|
|
/*
|
|
* InspectorObserver overrides
|
|
*/
|
|
|
|
void Connection::Impl::onBreakpointResolved(
|
|
Inspector &inspector,
|
|
const debugger::BreakpointInfo &info) {
|
|
m::debugger::BreakpointResolvedNotification note;
|
|
note.breakpointId = folly::to<std::string>(info.id);
|
|
note.location = m::debugger::makeLocation(info.resolvedLocation);
|
|
sendNotificationToClientViaExecutor(note);
|
|
}
|
|
|
|
void Connection::Impl::onContextCreated(Inspector &inspector) {
|
|
// Right now, Hermes only has the notion of one JS context per VM instance,
|
|
// so we just always name the single JS context with id=1 and name=hermes.
|
|
m::runtime::ExecutionContextCreatedNotification note;
|
|
note.context.id = kHermesExecutionContextId;
|
|
note.context.name = "hermes";
|
|
|
|
sendNotificationToClientViaExecutor(note);
|
|
}
|
|
|
|
void Connection::Impl::onPause(
|
|
Inspector &inspector,
|
|
const debugger::ProgramState &state) {
|
|
bool sendNotification = true;
|
|
m::debugger::PausedNotification note;
|
|
note.callFrames = m::debugger::makeCallFrames(state, objTable_, getRuntime());
|
|
|
|
switch (state.getPauseReason()) {
|
|
case debugger::PauseReason::Breakpoint:
|
|
// use other, chrome protocol has no reason specifically for breakpoints
|
|
note.reason = "other";
|
|
|
|
// TODO: hermes hasn't implemented ProgramState::getBreakpoint yet
|
|
#if HERMES_SUPPORTS_STATE_GET_BREAKPOINT
|
|
note.hitBreakpoints = std::vector<m::debugger::BreakpointId>();
|
|
note.hitBreakpoints->emplace_back(
|
|
folly::to<std::string>(state.getBreakpoint()));
|
|
#endif
|
|
|
|
break;
|
|
case debugger::PauseReason::Exception:
|
|
note.reason = "exception";
|
|
break;
|
|
case debugger::PauseReason::ScriptLoaded: {
|
|
// This case covers both wait-for-debugger and instrumentation
|
|
// breakpoints, since both are implemented as pauses on script load.
|
|
|
|
note.reason = "other";
|
|
note.hitBreakpoints = std::vector<m::debugger::BreakpointId>();
|
|
|
|
std::lock_guard<std::mutex> lock(virtualBreakpointMutex_);
|
|
for (auto &bp :
|
|
virtualBreakpoints_[kBeforeScriptWithSourceMapExecution]) {
|
|
note.hitBreakpoints->emplace_back(bp);
|
|
}
|
|
|
|
// Debuggers don't tend to ever remove these kinds of breakpoints, but
|
|
// in the extremely unlikely event that it did *and* did it exactly
|
|
// between us 1. checking that we should stop, and 2. adding the stop
|
|
// reason here, then just resume and skip sending a pause notification.
|
|
if (!inspector_->isAwaitingDebuggerOnStart() &&
|
|
note.hitBreakpoints->empty()) {
|
|
sendNotification = false;
|
|
inspector_->resume();
|
|
}
|
|
};
|
|
// This will be toggled back on in the next onScriptParsed if applicable
|
|
// Locking is handled by didPause in the inspector
|
|
inspector_->setPauseOnLoads(PauseOnLoadMode::None);
|
|
break;
|
|
default:
|
|
note.reason = "other";
|
|
break;
|
|
}
|
|
|
|
if (sendNotification) {
|
|
sendNotificationToClientViaExecutor(note);
|
|
}
|
|
}
|
|
|
|
void Connection::Impl::onResume(Inspector &inspector) {
|
|
objTable_.releaseObjectGroup(BacktraceObjectGroup);
|
|
|
|
m::debugger::ResumedNotification note;
|
|
sendNotificationToClientViaExecutor(note);
|
|
}
|
|
|
|
void Connection::Impl::onScriptParsed(
|
|
Inspector &inspector,
|
|
const ScriptInfo &info) {
|
|
m::debugger::ScriptParsedNotification note;
|
|
note.scriptId = folly::to<std::string>(info.fileId);
|
|
note.url = info.fileName;
|
|
// TODO(jpporto): fix test cases sending invalid context id.
|
|
// note.executionContextId = kHermesExecutionContextId;
|
|
|
|
if (!info.sourceMappingUrl.empty()) {
|
|
note.sourceMapURL = info.sourceMappingUrl;
|
|
|
|
std::lock_guard<std::mutex> lock(virtualBreakpointMutex_);
|
|
if (hasVirtualBreakpoint(kBeforeScriptWithSourceMapExecution)) {
|
|
// We are precariously relying on the fact that onScriptParsed
|
|
// is invoked immediately before the pause load mode is checked.
|
|
// That means that we can check for breakpoints and toggle the
|
|
// mode here, and then immediately turn it off in onPause.
|
|
inspector_->setPauseOnLoads(PauseOnLoadMode::All);
|
|
}
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(parsedScriptsMutex_);
|
|
parsedScripts_.push_back(info.fileName);
|
|
}
|
|
|
|
sendNotificationToClientViaExecutor(note);
|
|
}
|
|
|
|
void Connection::Impl::onMessageAdded(
|
|
facebook::hermes::inspector::Inspector &inspector,
|
|
const ConsoleMessageInfo &info) {
|
|
m::runtime::ConsoleAPICalledNotification apiCalledNote;
|
|
apiCalledNote.type = info.level;
|
|
// TODO(jpporto): fix test cases sending invalid context id.
|
|
// apiCalledNote.executionContextId = kHermesExecutionContextId;
|
|
|
|
size_t argsSize = info.args.size(getRuntime());
|
|
for (size_t index = 0; index < argsSize; ++index) {
|
|
apiCalledNote.args.push_back(m::runtime::makeRemoteObject(
|
|
getRuntime(),
|
|
info.args.getValueAtIndex(getRuntime(), index),
|
|
objTable_,
|
|
"ConsoleObjectGroup"));
|
|
}
|
|
|
|
sendNotificationToClientViaExecutor(apiCalledNote);
|
|
}
|
|
|
|
/*
|
|
* RequestHandler overrides
|
|
*/
|
|
|
|
void Connection::Impl::handle(const m::UnknownRequest &req) {
|
|
LOG(INFO) << "responding ok to unknown request: " << req.toDynamic();
|
|
sendResponseToClientViaExecutor(req.id);
|
|
}
|
|
|
|
void Connection::Impl::handle(const m::debugger::DisableRequest &req) {
|
|
sendResponseToClientViaExecutor(inspector_->disable(), req.id);
|
|
}
|
|
|
|
void Connection::Impl::handle(const m::debugger::EnableRequest &req) {
|
|
sendResponseToClientViaExecutor(inspector_->enable(), req.id);
|
|
}
|
|
|
|
void Connection::Impl::handle(
|
|
const m::debugger::EvaluateOnCallFrameRequest &req) {
|
|
auto remoteObjPtr = std::make_shared<m::runtime::RemoteObject>();
|
|
|
|
inspector_
|
|
->evaluate(
|
|
atoi(req.callFrameId.c_str()),
|
|
req.expression,
|
|
[this,
|
|
remoteObjPtr,
|
|
objectGroup = req.objectGroup,
|
|
byValue = req.returnByValue.value_or(false)](
|
|
const facebook::hermes::debugger::EvalResult
|
|
&evalResult) mutable {
|
|
*remoteObjPtr = m::runtime::makeRemoteObject(
|
|
getRuntime(),
|
|
evalResult.value,
|
|
objTable_,
|
|
objectGroup.value_or(""),
|
|
byValue);
|
|
})
|
|
.via(executor_.get())
|
|
.thenValue(
|
|
[this, id = req.id, remoteObjPtr](debugger::EvalResult result) {
|
|
m::debugger::EvaluateOnCallFrameResponse resp;
|
|
resp.id = id;
|
|
|
|
if (result.isException) {
|
|
resp.exceptionDetails =
|
|
m::runtime::makeExceptionDetails(result.exceptionDetails);
|
|
} else {
|
|
resp.result = *remoteObjPtr;
|
|
}
|
|
|
|
sendResponseToClient(resp);
|
|
})
|
|
.thenError<std::exception>(sendErrorToClient(req.id));
|
|
}
|
|
|
|
void Connection::Impl::sendSnapshot(
|
|
int reqId,
|
|
std::string message,
|
|
bool reportProgress,
|
|
bool stopStackTraceCapture) {
|
|
inspector_
|
|
->executeIfEnabled(
|
|
message,
|
|
[this, reportProgress, stopStackTraceCapture](
|
|
const debugger::ProgramState &) {
|
|
// Stop taking any new traces before sending out the heap snapshot.
|
|
if (stopStackTraceCapture) {
|
|
getRuntime()
|
|
.instrumentation()
|
|
.stopTrackingHeapObjectStackTraces();
|
|
}
|
|
|
|
if (reportProgress) {
|
|
// A progress notification with finished = true indicates the
|
|
// snapshot has been captured and is ready to be sent. Our
|
|
// implementation streams the snapshot as it is being captured, so
|
|
// we must send this notification first.
|
|
m::heapProfiler::ReportHeapSnapshotProgressNotification note;
|
|
note.done = 1;
|
|
note.total = 1;
|
|
note.finished = true;
|
|
sendNotificationToClient(note);
|
|
}
|
|
|
|
// Size picked to conform to Chrome's own implementation, at the
|
|
// time of writing.
|
|
inspector::detail::CallbackOStream cos(
|
|
/* sz */ 100 << 10, [this](std::string s) {
|
|
m::heapProfiler::AddHeapSnapshotChunkNotification note;
|
|
note.chunk = std::move(s);
|
|
sendNotificationToClient(note);
|
|
return true;
|
|
});
|
|
|
|
getRuntime().instrumentation().createSnapshotToStream(cos);
|
|
})
|
|
.via(executor_.get())
|
|
.thenValue([this, reqId](auto &&) {
|
|
sendResponseToClient(m::makeOkResponse(reqId));
|
|
})
|
|
.thenError<std::exception>(sendErrorToClient(reqId));
|
|
}
|
|
|
|
void Connection::Impl::handle(
|
|
const m::heapProfiler::TakeHeapSnapshotRequest &req) {
|
|
sendSnapshot(
|
|
req.id,
|
|
"HeapSnapshot.takeHeapSnapshot",
|
|
req.reportProgress && *req.reportProgress,
|
|
/* stopStackTraceCapture */ false);
|
|
}
|
|
|
|
void Connection::Impl::handle(
|
|
const m::heapProfiler::StartTrackingHeapObjectsRequest &req) {
|
|
const auto id = req.id;
|
|
|
|
inspector_
|
|
->executeIfEnabled(
|
|
"HeapProfiler.startTrackingHeapObjects",
|
|
[this](const debugger::ProgramState &) {
|
|
getRuntime().instrumentation().startTrackingHeapObjectStackTraces(
|
|
[this](
|
|
uint64_t lastSeenObjectId,
|
|
std::chrono::microseconds timestamp,
|
|
std::vector<jsi::Instrumentation::HeapStatsUpdate> stats) {
|
|
// Send the last object ID notification first.
|
|
m::heapProfiler::LastSeenObjectIdNotification note;
|
|
note.lastSeenObjectId = lastSeenObjectId;
|
|
// The protocol uses milliseconds with a fraction for
|
|
// microseconds.
|
|
note.timestamp =
|
|
static_cast<double>(timestamp.count()) / 1000;
|
|
sendNotificationToClient(note);
|
|
|
|
m::heapProfiler::HeapStatsUpdateNotification heapStatsNote;
|
|
// Flatten the HeapStatsUpdate list.
|
|
heapStatsNote.statsUpdate.reserve(stats.size() * 3);
|
|
for (const jsi::Instrumentation::HeapStatsUpdate &fragment :
|
|
stats) {
|
|
// Each triplet is the fragment number, the total count of
|
|
// objects for the fragment, and the total size of objects
|
|
// for the fragment.
|
|
heapStatsNote.statsUpdate.push_back(
|
|
static_cast<int>(std::get<0>(fragment)));
|
|
heapStatsNote.statsUpdate.push_back(
|
|
static_cast<int>(std::get<1>(fragment)));
|
|
heapStatsNote.statsUpdate.push_back(
|
|
static_cast<int>(std::get<2>(fragment)));
|
|
}
|
|
assert(
|
|
heapStatsNote.statsUpdate.size() == stats.size() * 3 &&
|
|
"Should be exactly 3x the stats vector");
|
|
// TODO: Chunk this if there are too many fragments to update.
|
|
// Unlikely to be a problem in practice unless there's a huge
|
|
// amount of allocation and freeing.
|
|
sendNotificationToClient(heapStatsNote);
|
|
});
|
|
// At this point we need the equivalent of a setInterval, where each
|
|
// interval samples the existing
|
|
})
|
|
.via(executor_.get())
|
|
.thenValue(
|
|
[this, id](auto &&) { sendResponseToClient(m::makeOkResponse(id)); })
|
|
.thenError<std::exception>(sendErrorToClient(req.id));
|
|
}
|
|
|
|
void Connection::Impl::handle(
|
|
const m::heapProfiler::StopTrackingHeapObjectsRequest &req) {
|
|
sendSnapshot(
|
|
req.id,
|
|
"HeapSnapshot.stopTrackingHeapObjects",
|
|
req.reportProgress && *req.reportProgress,
|
|
/* stopStackTraceCapture */ true);
|
|
}
|
|
|
|
void Connection::Impl::handle(
|
|
const m::heapProfiler::StartSamplingRequest &req) {
|
|
const auto id = req.id;
|
|
// This is the same default sampling interval that Chrome uses.
|
|
// https://chromedevtools.github.io/devtools-protocol/tot/HeapProfiler/#method-startSampling
|
|
constexpr size_t kDefaultSamplingInterval = 1 << 15;
|
|
const size_t samplingInterval =
|
|
req.samplingInterval.value_or(kDefaultSamplingInterval);
|
|
|
|
inspector_
|
|
->executeIfEnabled(
|
|
"HeapProfiler.startSampling",
|
|
[this, samplingInterval](const debugger::ProgramState &) {
|
|
getRuntime().instrumentation().startHeapSampling(samplingInterval);
|
|
})
|
|
.via(executor_.get())
|
|
.thenValue(
|
|
[this, id](auto &&) { sendResponseToClient(m::makeOkResponse(id)); })
|
|
.thenError<std::exception>(sendErrorToClient(req.id));
|
|
}
|
|
|
|
void Connection::Impl::handle(const m::heapProfiler::StopSamplingRequest &req) {
|
|
inspector_
|
|
->executeIfEnabled(
|
|
"HeapProfiler.stopSampling",
|
|
[this, id = req.id](const debugger::ProgramState &) {
|
|
std::ostringstream stream;
|
|
getRuntime().instrumentation().stopHeapSampling(stream);
|
|
folly::dynamic json = folly::parseJson(stream.str());
|
|
m::heapProfiler::StopSamplingResponse resp;
|
|
resp.id = id;
|
|
m::heapProfiler::SamplingHeapProfile profile{json};
|
|
resp.profile = profile;
|
|
sendResponseToClient(resp);
|
|
})
|
|
.via(executor_.get())
|
|
.thenError<std::exception>(sendErrorToClient(req.id));
|
|
}
|
|
|
|
void Connection::Impl::handle(
|
|
const m::heapProfiler::CollectGarbageRequest &req) {
|
|
const auto id = req.id;
|
|
|
|
inspector_
|
|
->executeIfEnabled(
|
|
"HeapProfiler.collectGarbage",
|
|
[this](const debugger::ProgramState &) {
|
|
getRuntime().instrumentation().collectGarbage("inspector");
|
|
})
|
|
.via(executor_.get())
|
|
.thenValue(
|
|
[this, id](auto &&) { sendResponseToClient(m::makeOkResponse(id)); })
|
|
.thenError<std::exception>(sendErrorToClient(req.id));
|
|
}
|
|
|
|
void Connection::Impl::handle(
|
|
const m::heapProfiler::GetObjectByHeapObjectIdRequest &req) {
|
|
uint64_t objID = atoi(req.objectId.c_str());
|
|
folly::Optional<std::string> group = req.objectGroup;
|
|
auto remoteObjPtr = std::make_shared<m::runtime::RemoteObject>();
|
|
|
|
inspector_
|
|
->executeIfEnabled(
|
|
"HeapProfiler.getObjectByHeapObjectId",
|
|
[this, remoteObjPtr, objID, group](const debugger::ProgramState &) {
|
|
jsi::Runtime *rt = &getRuntime();
|
|
if (auto *hermesRT = dynamic_cast<HermesRuntime *>(rt)) {
|
|
jsi::Value val = hermesRT->getObjectForID(objID);
|
|
if (val.isNull()) {
|
|
return;
|
|
}
|
|
*remoteObjPtr = m::runtime::makeRemoteObject(
|
|
getRuntime(), val, objTable_, group.value_or(""));
|
|
}
|
|
})
|
|
.via(executor_.get())
|
|
.thenValue([this, id = req.id, remoteObjPtr](auto &&) {
|
|
if (!remoteObjPtr->type.empty()) {
|
|
m::heapProfiler::GetObjectByHeapObjectIdResponse resp;
|
|
resp.id = id;
|
|
resp.result = *remoteObjPtr;
|
|
sendResponseToClient(resp);
|
|
} else {
|
|
sendResponseToClient(m::makeErrorResponse(
|
|
id, m::ErrorCode::ServerError, "Object is not available"));
|
|
}
|
|
})
|
|
.thenError<std::exception>(sendErrorToClient(req.id));
|
|
}
|
|
|
|
void Connection::Impl::handle(
|
|
const m::heapProfiler::GetHeapObjectIdRequest &req) {
|
|
// Use a shared_ptr because the stack frame will go away.
|
|
std::shared_ptr<uint64_t> snapshotID = std::make_shared<uint64_t>(0);
|
|
|
|
inspector_
|
|
->executeIfEnabled(
|
|
"HeapProfiler.getHeapObjectId",
|
|
[this, req, snapshotID](const debugger::ProgramState &) {
|
|
if (const jsi::Value *valuePtr = objTable_.getValue(req.objectId)) {
|
|
jsi::Runtime *rt = &getRuntime();
|
|
if (auto *hermesRT = dynamic_cast<HermesRuntime *>(rt)) {
|
|
*snapshotID = hermesRT->getUniqueID(*valuePtr);
|
|
}
|
|
}
|
|
})
|
|
.via(executor_.get())
|
|
.thenValue([this, id = req.id, snapshotID](auto &&) {
|
|
if (*snapshotID) {
|
|
m::heapProfiler::GetHeapObjectIdResponse resp;
|
|
resp.id = id;
|
|
// std::to_string is not available on Android, use a std::ostream
|
|
// instead.
|
|
std::ostringstream stream;
|
|
stream << *snapshotID;
|
|
resp.heapSnapshotObjectId = stream.str();
|
|
sendResponseToClient(resp);
|
|
} else {
|
|
sendResponseToClient(m::makeErrorResponse(
|
|
id, m::ErrorCode::ServerError, "Object is not available"));
|
|
}
|
|
})
|
|
.thenError<std::exception>(sendErrorToClient(req.id));
|
|
}
|
|
|
|
namespace {
|
|
/// Runtime.CallArguments can have their values specified "inline", or they can
|
|
/// have remote references. The inline values are eval'd together with the
|
|
/// Runtime.CallFunctionOn.functionDeclaration (see CallFunctionOnBuilder
|
|
/// below), while remote object Ids need to be resolved outside of the VM.
|
|
class CallFunctionOnArgument {
|
|
public:
|
|
explicit CallFunctionOnArgument(
|
|
folly::Optional<m::runtime::RemoteObjectId> maybeObjectId)
|
|
: maybeObjectId_(std::move(maybeObjectId)) {}
|
|
|
|
/// Computes the real value for this argument, which can be an object
|
|
/// referenced by maybeObjectId_, or the given evaldValue. Throws if
|
|
/// maybeObjectId_ is not empty but references an unknown object.
|
|
jsi::Value value(
|
|
jsi::Runtime &rt,
|
|
RemoteObjectsTable &objTable,
|
|
jsi::Value evaldValue) const {
|
|
if (maybeObjectId_) {
|
|
assert(evaldValue.isUndefined() && "expected undefined placeholder");
|
|
return getValueFromId(rt, objTable, *maybeObjectId_);
|
|
}
|
|
|
|
return evaldValue;
|
|
}
|
|
|
|
private:
|
|
/// Returns the jsi::Object for the given objId. Throws if such object can't
|
|
/// be found.
|
|
static jsi::Value getValueFromId(
|
|
jsi::Runtime &rt,
|
|
RemoteObjectsTable &objTable,
|
|
m::runtime::RemoteObjectId objId) {
|
|
if (const jsi::Value *ptr = objTable.getValue(objId)) {
|
|
return jsi::Value(rt, *ptr);
|
|
}
|
|
|
|
throw std::runtime_error("unknown object id " + objId);
|
|
}
|
|
|
|
folly::Optional<m::runtime::RemoteObjectId> maybeObjectId_;
|
|
};
|
|
|
|
/// Functor that should be used to run the result of eval-ing a CallFunctionOn
|
|
/// request.
|
|
class CallFunctionOnRunner {
|
|
public:
|
|
static constexpr size_t kJsThisIndex = 0;
|
|
static constexpr size_t kFirstArgIndex = 1;
|
|
|
|
// N.B.: constexpr char[] broke react-native-oss-android.
|
|
static const char *kJsThisArgPlaceholder;
|
|
|
|
CallFunctionOnRunner() = default;
|
|
CallFunctionOnRunner(CallFunctionOnRunner &&) = default;
|
|
CallFunctionOnRunner &operator=(CallFunctionOnRunner &&) = default;
|
|
|
|
/// Performs the actual Runtime.CallFunctionOn request. It assumes.
|
|
/// \p evalResult is the result of invoking the Inspector's evaluate() method
|
|
/// on the expression built by the CallFunctionOnBuilder below.
|
|
jsi::Value operator()(
|
|
jsi::Runtime &rt,
|
|
RemoteObjectsTable &objTable,
|
|
const facebook::hermes::debugger::EvalResult &evalResult) {
|
|
// The eval result is an array [a0, a1, ..., an, func] (see
|
|
// CallFunctionOnBuilder below).
|
|
auto argsAndFunc = evalResult.value.getObject(rt).getArray(rt);
|
|
assert(
|
|
argsAndFunc.length(rt) == thisAndArguments_.size() + 1 &&
|
|
"Unexpected result size");
|
|
|
|
// now resolve the arguments to the call, including "this".
|
|
std::vector<jsi::Value> arguments(thisAndArguments_.size() - 1);
|
|
|
|
jsi::Object jsThis =
|
|
getJsThis(rt, objTable, argsAndFunc.getValueAtIndex(rt, kJsThisIndex));
|
|
|
|
int i = kFirstArgIndex;
|
|
for (/*i points to the first param*/; i < thisAndArguments_.size(); ++i) {
|
|
arguments[i - kFirstArgIndex] = thisAndArguments_[i].value(
|
|
rt, objTable, argsAndFunc.getValueAtIndex(rt, i));
|
|
}
|
|
|
|
// i is now func's index.
|
|
jsi::Function func =
|
|
argsAndFunc.getValueAtIndex(rt, i).getObject(rt).getFunction(rt);
|
|
|
|
return func.callWithThis(
|
|
rt,
|
|
std::move(jsThis),
|
|
static_cast<const jsi::Value *>(arguments.data()),
|
|
arguments.size());
|
|
}
|
|
|
|
private:
|
|
friend class CallFunctionOnBuilder;
|
|
|
|
CallFunctionOnRunner(const CallFunctionOnRunner &) = delete;
|
|
CallFunctionOnRunner &operator=(const CallFunctionOnRunner &) = delete;
|
|
|
|
CallFunctionOnRunner(
|
|
std::vector<CallFunctionOnArgument> thisAndArguments,
|
|
folly::Optional<m::runtime::ExecutionContextId> executionContextId)
|
|
: thisAndArguments_(std::move(thisAndArguments)),
|
|
executionContextId_(std::move(executionContextId)) {}
|
|
|
|
/// Resolves the js "this" for the request, which lives in
|
|
/// thisAndArguments_[kJsThisIndex]. \p evaldThis should either be undefined,
|
|
/// or the placeholder indicating that globalThis should be used.
|
|
jsi::Object getJsThis(
|
|
jsi::Runtime &rt,
|
|
RemoteObjectsTable &objTable,
|
|
jsi::Value evaldThis) const {
|
|
// In the future we may support multiple execution context ids; for now,
|
|
// there's only one.
|
|
(void)executionContextId_;
|
|
|
|
// Either evaldThis is undefined (because the request had an object id
|
|
// specifying "this"), or it should be a string (i.e., the placeholder
|
|
// kJsThisArgPlaceholder).
|
|
assert(
|
|
(evaldThis.isUndefined() ||
|
|
(evaldThis.isString() &&
|
|
evaldThis.getString(rt).utf8(rt) == kJsThisArgPlaceholder)) &&
|
|
"unexpected value for jsThis argument placeholder");
|
|
|
|
// Need to save this information because of the std::move() below.
|
|
const bool useGlobalThis = evaldThis.isString();
|
|
jsi::Value value = thisAndArguments_[kJsThisIndex].value(
|
|
rt, objTable, std::move(evaldThis));
|
|
|
|
return useGlobalThis ? rt.global() : value.getObject(rt);
|
|
}
|
|
|
|
std::vector<CallFunctionOnArgument> thisAndArguments_;
|
|
folly::Optional<m::runtime::ExecutionContextId> executionContextId_;
|
|
};
|
|
|
|
/*static*/ const char *CallFunctionOnRunner::kJsThisArgPlaceholder =
|
|
"jsThis is Execution Context";
|
|
|
|
/// Returns true if \p str is a number-like string value (e.g., Infinity),
|
|
/// and false otherwise.
|
|
bool unserializableValueLooksLikeNumber(const std::string &str) {
|
|
return str == "Infinity" || str == "-Infinity" || str == "NaN";
|
|
}
|
|
|
|
/// Helper class that processes a Runtime.CallFunctionOn request, and
|
|
/// builds an expression string that, once eval()d, yields an Array with the
|
|
/// CallArguments as well as the function to run. The generated array is
|
|
///
|
|
/// [JsThis, P0, P1, P2, P3, Pn, F]
|
|
///
|
|
/// where:
|
|
/// * F is the functionDeclaration in the request
|
|
/// * JsThis is either:
|
|
/// * undefined (if the request has an object ID); or
|
|
/// * the placeholder kJsThisArgPlaceholder
|
|
/// * Pi is either:
|
|
/// * the string in CallArgument[i].unserializableValue; or
|
|
/// * the string in CallArgument[i].value; or
|
|
/// * arguments[j] (i.e., the j-th argument passed to the newly built
|
|
/// function), j being the j-th CallArgument with an ObjectId. This is
|
|
/// needed because there's no easy way to express the objects referred
|
|
/// to by object ids by name.
|
|
class CallFunctionOnBuilder {
|
|
public:
|
|
explicit CallFunctionOnBuilder(const m::runtime::CallFunctionOnRequest &req)
|
|
: executionContextId_(req.executionContextId) {
|
|
out_ << "[";
|
|
thisAndArguments_.emplace_back(CallFunctionOnArgument(req.objectId));
|
|
if (req.objectId) {
|
|
out_ << "undefined, ";
|
|
} else {
|
|
out_ << '\'' << CallFunctionOnRunner::kJsThisArgPlaceholder << "', ";
|
|
}
|
|
|
|
addParams(req.arguments);
|
|
out_ << req.functionDeclaration;
|
|
out_ << "]";
|
|
};
|
|
|
|
/// Extracts the functions that handles the CallFunctionOn requests, as well
|
|
/// as the list of object ids that must be passed when calling it.
|
|
std::pair<std::string, CallFunctionOnRunner> expressionAndRunner() && {
|
|
return std::make_pair(
|
|
std::move(out_).str(),
|
|
CallFunctionOnRunner(
|
|
std::move(thisAndArguments_), std::move(executionContextId_)));
|
|
}
|
|
|
|
private:
|
|
void addParams(const folly::Optional<std::vector<m::runtime::CallArgument>>
|
|
&maybeArguments) {
|
|
if (maybeArguments) {
|
|
for (const auto &ca : *maybeArguments) {
|
|
addParam(ca);
|
|
thisAndArguments_.emplace_back(CallFunctionOnArgument(ca.objectId));
|
|
out_ << ", ";
|
|
}
|
|
}
|
|
}
|
|
|
|
void addParam(const m::runtime::CallArgument &ca) {
|
|
if (ca.objectId) {
|
|
out_ << "undefined";
|
|
} else if (ca.value) {
|
|
// TODO: this may throw if ca.value is a CBOR (see RFC 8949), but the
|
|
// chrome debugger doesn't seem to send those.
|
|
out_ << "(" << folly::toJson(*ca.value) << ")";
|
|
} else if (ca.unserializableValue) {
|
|
if (unserializableValueLooksLikeNumber(*ca.unserializableValue)) {
|
|
out_ << "+(" << *ca.unserializableValue << ")";
|
|
} else {
|
|
out_ << *ca.unserializableValue;
|
|
}
|
|
} else {
|
|
throw std::runtime_error("unknown payload for CallParam");
|
|
}
|
|
}
|
|
|
|
std::ostringstream out_;
|
|
|
|
std::vector<CallFunctionOnArgument> thisAndArguments_;
|
|
folly::Optional<m::runtime::ExecutionContextId> executionContextId_;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
void Connection::Impl::handle(const m::runtime::CallFunctionOnRequest &req) {
|
|
std::string expression;
|
|
CallFunctionOnRunner runner;
|
|
|
|
auto validateAndParseRequest =
|
|
[&expression, &runner](const m::runtime::CallFunctionOnRequest &req)
|
|
-> folly::Optional<std::string> {
|
|
if (req.objectId.hasValue() == req.executionContextId.hasValue()) {
|
|
return std::string(
|
|
"The request must specify either object id or execution context id.");
|
|
}
|
|
|
|
if (!req.objectId) {
|
|
assert(
|
|
req.executionContextId &&
|
|
"should not be here if both object id and execution context id are missing");
|
|
if (*req.executionContextId != kHermesExecutionContextId) {
|
|
return "unknown execution context id " +
|
|
std::to_string(*req.executionContextId);
|
|
}
|
|
}
|
|
|
|
try {
|
|
std::tie(expression, runner) =
|
|
CallFunctionOnBuilder(req).expressionAndRunner();
|
|
} catch (const std::exception &e) {
|
|
return std::string(e.what());
|
|
}
|
|
|
|
return {};
|
|
};
|
|
|
|
if (auto errMsg = validateAndParseRequest(req)) {
|
|
sendErrorToClientViaExecutor(req.id, *errMsg);
|
|
return;
|
|
}
|
|
|
|
auto remoteObjPtr = std::make_shared<m::runtime::RemoteObject>();
|
|
inspector_
|
|
->evaluate(
|
|
0, // Top of the stackframe
|
|
expression,
|
|
[this,
|
|
remoteObjPtr,
|
|
objectGroup = req.objectGroup,
|
|
jsThisId = req.objectId,
|
|
executionContextId = req.executionContextId,
|
|
byValue = req.returnByValue.value_or(false),
|
|
runner =
|
|
std::move(runner)](const facebook::hermes::debugger::EvalResult
|
|
&evalResult) mutable {
|
|
if (evalResult.isException) {
|
|
return;
|
|
}
|
|
|
|
*remoteObjPtr = m::runtime::makeRemoteObject(
|
|
getRuntime(),
|
|
runner(getRuntime(), objTable_, evalResult),
|
|
objTable_,
|
|
objectGroup.value_or("ConsoleObjectGroup"),
|
|
byValue);
|
|
})
|
|
.via(executor_.get())
|
|
.thenValue(
|
|
[this, id = req.id, remoteObjPtr](debugger::EvalResult result) {
|
|
m::debugger::EvaluateOnCallFrameResponse resp;
|
|
resp.id = id;
|
|
|
|
if (result.isException) {
|
|
resp.exceptionDetails =
|
|
m::runtime::makeExceptionDetails(result.exceptionDetails);
|
|
} else {
|
|
resp.result = *remoteObjPtr;
|
|
}
|
|
|
|
sendResponseToClient(resp);
|
|
})
|
|
.thenError<std::exception>(sendErrorToClient(req.id));
|
|
}
|
|
|
|
void Connection::Impl::handle(const m::runtime::EvaluateRequest &req) {
|
|
auto remoteObjPtr = std::make_shared<m::runtime::RemoteObject>();
|
|
|
|
inspector_
|
|
->evaluate(
|
|
0, // Top of the stackframe
|
|
req.expression,
|
|
[this,
|
|
remoteObjPtr,
|
|
objectGroup = req.objectGroup,
|
|
byValue = req.returnByValue.value_or(false)](
|
|
const facebook::hermes::debugger::EvalResult
|
|
&evalResult) mutable {
|
|
*remoteObjPtr = m::runtime::makeRemoteObject(
|
|
getRuntime(),
|
|
evalResult.value,
|
|
objTable_,
|
|
objectGroup.value_or("ConsoleObjectGroup"),
|
|
byValue);
|
|
})
|
|
.via(executor_.get())
|
|
.thenValue(
|
|
[this, id = req.id, remoteObjPtr](debugger::EvalResult result) {
|
|
m::debugger::EvaluateOnCallFrameResponse resp;
|
|
resp.id = id;
|
|
|
|
if (result.isException) {
|
|
resp.exceptionDetails =
|
|
m::runtime::makeExceptionDetails(result.exceptionDetails);
|
|
} else {
|
|
resp.result = *remoteObjPtr;
|
|
}
|
|
|
|
sendResponseToClient(resp);
|
|
})
|
|
.thenError<std::exception>(sendErrorToClient(req.id));
|
|
}
|
|
|
|
void Connection::Impl::handle(const m::debugger::PauseRequest &req) {
|
|
sendResponseToClientViaExecutor(inspector_->pause(), req.id);
|
|
}
|
|
|
|
void Connection::Impl::handle(const m::debugger::RemoveBreakpointRequest &req) {
|
|
if (isVirtualBreakpointId(req.breakpointId)) {
|
|
std::lock_guard<std::mutex> lock(virtualBreakpointMutex_);
|
|
if (!removeVirtualBreakpoint(req.breakpointId)) {
|
|
sendErrorToClientViaExecutor(
|
|
req.id, "Unknown breakpoint ID: " + req.breakpointId);
|
|
}
|
|
sendResponseToClientViaExecutor(req.id);
|
|
} else {
|
|
auto breakpointId = folly::to<debugger::BreakpointID>(req.breakpointId);
|
|
sendResponseToClientViaExecutor(
|
|
inspector_->removeBreakpoint(breakpointId), req.id);
|
|
}
|
|
}
|
|
|
|
void Connection::Impl::handle(const m::debugger::ResumeRequest &req) {
|
|
sendResponseToClientViaExecutor(inspector_->resume(), req.id);
|
|
}
|
|
|
|
void Connection::Impl::handle(const m::debugger::SetBreakpointRequest &req) {
|
|
debugger::SourceLocation loc;
|
|
|
|
auto scriptId = folly::tryTo<unsigned int>(req.location.scriptId);
|
|
if (!scriptId) {
|
|
sendErrorToClientViaExecutor(
|
|
req.id, "Expected integer scriptId: " + req.location.scriptId);
|
|
return;
|
|
}
|
|
|
|
loc.fileId = scriptId.value();
|
|
// CDP Locations are 0-based, Hermes lines/columns are 1-based
|
|
loc.line = req.location.lineNumber + 1;
|
|
if (req.location.columnNumber) {
|
|
loc.column = req.location.columnNumber.value() + 1;
|
|
}
|
|
|
|
inspector_->setBreakpoint(loc, req.condition)
|
|
.via(executor_.get())
|
|
.thenValue([this, id = req.id](debugger::BreakpointInfo info) {
|
|
m::debugger::SetBreakpointResponse resp;
|
|
resp.id = id;
|
|
resp.breakpointId = folly::to<std::string>(info.id);
|
|
|
|
if (info.resolved) {
|
|
resp.actualLocation =
|
|
m::debugger::makeLocation(info.resolvedLocation);
|
|
}
|
|
|
|
sendResponseToClient(resp);
|
|
})
|
|
.thenError<std::exception>(sendErrorToClient(req.id));
|
|
}
|
|
|
|
void Connection::Impl::handle(
|
|
const m::debugger::SetBreakpointByUrlRequest &req) {
|
|
debugger::SourceLocation loc;
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(parsedScriptsMutex_);
|
|
setHermesLocation(loc, req, parsedScripts_);
|
|
}
|
|
|
|
inspector_->setBreakpoint(loc, req.condition)
|
|
.via(executor_.get())
|
|
.thenValue([this, id = req.id](debugger::BreakpointInfo info) {
|
|
m::debugger::SetBreakpointByUrlResponse resp;
|
|
resp.id = id;
|
|
resp.breakpointId = folly::to<std::string>(info.id);
|
|
|
|
if (info.resolved) {
|
|
resp.locations.emplace_back(
|
|
m::debugger::makeLocation(info.resolvedLocation));
|
|
}
|
|
|
|
sendResponseToClient(resp);
|
|
})
|
|
.thenError<std::exception>(sendErrorToClient(req.id));
|
|
}
|
|
|
|
void Connection::Impl::handle(
|
|
const m::debugger::SetBreakpointsActiveRequest &req) {
|
|
inspector_->setBreakpointsActive(req.active)
|
|
.via(executor_.get())
|
|
.thenValue([this, id = req.id](const Unit &unit) {
|
|
sendResponseToClient(m::makeOkResponse(id));
|
|
})
|
|
.thenError<std::exception>(sendErrorToClient(req.id));
|
|
}
|
|
|
|
bool Connection::Impl::isVirtualBreakpointId(const std::string &id) {
|
|
return id.rfind(kVirtualBreakpointPrefix, 0) == 0;
|
|
}
|
|
|
|
const std::string &Connection::Impl::createVirtualBreakpoint(
|
|
const std::string &category) {
|
|
auto ret = virtualBreakpoints_[category].insert(folly::to<std::string>(
|
|
kVirtualBreakpointPrefix, nextVirtualBreakpoint_++));
|
|
return *ret.first;
|
|
}
|
|
|
|
bool Connection::Impl::hasVirtualBreakpoint(const std::string &category) {
|
|
auto pos = virtualBreakpoints_.find(category);
|
|
if (pos == virtualBreakpoints_.end())
|
|
return false;
|
|
return !pos->second.empty();
|
|
}
|
|
|
|
bool Connection::Impl::removeVirtualBreakpoint(const std::string &id) {
|
|
// We expect roughly 1 category, so just iterate over all the sets
|
|
for (auto &kv : virtualBreakpoints_) {
|
|
if (kv.second.erase(id) > 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Connection::Impl::handle(
|
|
const m::debugger::SetInstrumentationBreakpointRequest &req) {
|
|
if (req.instrumentation != kBeforeScriptWithSourceMapExecution) {
|
|
sendErrorToClientViaExecutor(
|
|
req.id, "Unknown instrumentation breakpoint: " + req.instrumentation);
|
|
return;
|
|
}
|
|
|
|
// The act of creating and registering the breakpoint ID is enough
|
|
// to "set" it. We merely check for the existence of them later.
|
|
std::lock_guard<std::mutex> lock(virtualBreakpointMutex_);
|
|
m::debugger::SetInstrumentationBreakpointResponse resp;
|
|
resp.id = req.id;
|
|
resp.breakpointId = createVirtualBreakpoint(req.instrumentation);
|
|
sendResponseToClientViaExecutor(resp);
|
|
}
|
|
|
|
void Connection::Impl::handle(
|
|
const m::debugger::SetPauseOnExceptionsRequest &req) {
|
|
debugger::PauseOnThrowMode mode = debugger::PauseOnThrowMode::None;
|
|
|
|
if (req.state == "none") {
|
|
mode = debugger::PauseOnThrowMode::None;
|
|
} else if (req.state == "all") {
|
|
mode = debugger::PauseOnThrowMode::All;
|
|
} else if (req.state == "uncaught") {
|
|
mode = debugger::PauseOnThrowMode::Uncaught;
|
|
} else {
|
|
sendErrorToClientViaExecutor(
|
|
req.id, "Unknown pause-on-exception state: " + req.state);
|
|
return;
|
|
}
|
|
|
|
sendResponseToClientViaExecutor(
|
|
inspector_->setPauseOnExceptions(mode), req.id);
|
|
}
|
|
|
|
void Connection::Impl::handle(const m::debugger::StepIntoRequest &req) {
|
|
sendResponseToClientViaExecutor(inspector_->stepIn(), req.id);
|
|
}
|
|
|
|
void Connection::Impl::handle(const m::debugger::StepOutRequest &req) {
|
|
sendResponseToClientViaExecutor(inspector_->stepOut(), req.id);
|
|
}
|
|
|
|
void Connection::Impl::handle(const m::debugger::StepOverRequest &req) {
|
|
sendResponseToClientViaExecutor(inspector_->stepOver(), req.id);
|
|
}
|
|
|
|
std::vector<m::runtime::PropertyDescriptor>
|
|
Connection::Impl::makePropsFromScope(
|
|
std::pair<uint32_t, uint32_t> frameAndScopeIndex,
|
|
const std::string &objectGroup,
|
|
const debugger::ProgramState &state) {
|
|
// Chrome represents variables in a scope as properties on a dummy object.
|
|
// We don't instantiate such dummy objects, we just pretended to have one.
|
|
// Chrome has now asked for its properties, so it's time to synthesize
|
|
// descriptions of the properties that the dummy object would have had.
|
|
std::vector<m::runtime::PropertyDescriptor> result;
|
|
|
|
uint32_t frameIndex = frameAndScopeIndex.first;
|
|
uint32_t scopeIndex = frameAndScopeIndex.second;
|
|
debugger::LexicalInfo lexicalInfo = state.getLexicalInfo(frameIndex);
|
|
uint32_t varCount = lexicalInfo.getVariablesCountInScope(scopeIndex);
|
|
|
|
// If this is the frame's local scope, include 'this'.
|
|
if (scopeIndex == 0) {
|
|
auto varInfo = state.getVariableInfoForThis(frameIndex);
|
|
m::runtime::PropertyDescriptor desc;
|
|
desc.name = varInfo.name;
|
|
desc.value = m::runtime::makeRemoteObject(
|
|
getRuntime(), varInfo.value, objTable_, objectGroup);
|
|
// Chrome only shows enumerable properties.
|
|
desc.enumerable = true;
|
|
result.emplace_back(std::move(desc));
|
|
}
|
|
|
|
// Then add each of the variables in this lexical scope.
|
|
for (uint32_t varIndex = 0; varIndex < varCount; varIndex++) {
|
|
debugger::VariableInfo varInfo =
|
|
state.getVariableInfo(frameIndex, scopeIndex, varIndex);
|
|
|
|
m::runtime::PropertyDescriptor desc;
|
|
desc.name = varInfo.name;
|
|
desc.value = m::runtime::makeRemoteObject(
|
|
getRuntime(), varInfo.value, objTable_, objectGroup);
|
|
desc.enumerable = true;
|
|
|
|
result.emplace_back(std::move(desc));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::vector<m::runtime::PropertyDescriptor>
|
|
Connection::Impl::makePropsFromValue(
|
|
const jsi::Value &value,
|
|
const std::string &objectGroup,
|
|
bool onlyOwnProperties) {
|
|
std::vector<m::runtime::PropertyDescriptor> result;
|
|
|
|
if (value.isObject()) {
|
|
jsi::Runtime &runtime = getRuntime();
|
|
jsi::Object obj = value.getObject(runtime);
|
|
|
|
// TODO(hypuk): obj.getPropertyNames only returns enumerable properties.
|
|
jsi::Array propNames = onlyOwnProperties
|
|
? runtime.global()
|
|
.getPropertyAsObject(runtime, "Object")
|
|
.getPropertyAsFunction(runtime, "getOwnPropertyNames")
|
|
.call(runtime, obj)
|
|
.getObject(runtime)
|
|
.getArray(runtime)
|
|
: obj.getPropertyNames(runtime);
|
|
|
|
size_t propCount = propNames.length(runtime);
|
|
for (size_t i = 0; i < propCount; i++) {
|
|
jsi::String propName =
|
|
propNames.getValueAtIndex(runtime, i).getString(runtime);
|
|
|
|
m::runtime::PropertyDescriptor desc;
|
|
desc.name = propName.utf8(runtime);
|
|
|
|
try {
|
|
// Currently, we fetch the property even if it runs code.
|
|
// Chrome instead detects getters and makes you click to invoke.
|
|
jsi::Value propValue = obj.getProperty(runtime, propName);
|
|
desc.value = m::runtime::makeRemoteObject(
|
|
runtime, propValue, objTable_, objectGroup);
|
|
} catch (const jsi::JSError &err) {
|
|
// We fetched a property with a getter that threw. Show a placeholder.
|
|
// We could have added additional info, but the UI quickly gets messy.
|
|
desc.value = m::runtime::makeRemoteObject(
|
|
runtime,
|
|
jsi::String::createFromUtf8(runtime, "(Exception)"),
|
|
objTable_,
|
|
objectGroup);
|
|
}
|
|
|
|
result.emplace_back(std::move(desc));
|
|
}
|
|
|
|
if (onlyOwnProperties) {
|
|
jsi::Value proto = runtime.global()
|
|
.getPropertyAsObject(runtime, "Object")
|
|
.getPropertyAsFunction(runtime, "getPrototypeOf")
|
|
.call(runtime, obj);
|
|
if (!proto.isNull()) {
|
|
m::runtime::PropertyDescriptor desc;
|
|
desc.name = "__proto__";
|
|
desc.value = m::runtime::makeRemoteObject(
|
|
runtime, proto, objTable_, objectGroup);
|
|
result.emplace_back(std::move(desc));
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void Connection::Impl::handle(const m::runtime::GetHeapUsageRequest &req) {
|
|
auto resp = std::make_shared<m::runtime::GetHeapUsageResponse>();
|
|
resp->id = req.id;
|
|
|
|
inspector_
|
|
->executeIfEnabled(
|
|
"Runtime.getHeapUsage",
|
|
[this, req, resp](const debugger::ProgramState &state) {
|
|
auto heapInfo = getRuntime().instrumentation().getHeapInfo(false);
|
|
resp->usedSize = heapInfo["hermes_allocatedBytes"];
|
|
resp->totalSize = heapInfo["hermes_heapSize"];
|
|
})
|
|
.via(executor_.get())
|
|
.thenValue([this, resp](auto &&) { sendResponseToClient(*resp); })
|
|
.thenError<std::exception>(sendErrorToClient(req.id));
|
|
}
|
|
|
|
void Connection::Impl::handle(const m::runtime::GetPropertiesRequest &req) {
|
|
auto resp = std::make_shared<m::runtime::GetPropertiesResponse>();
|
|
resp->id = req.id;
|
|
|
|
inspector_
|
|
->executeIfEnabled(
|
|
"Runtime.getProperties",
|
|
[this, req, resp](const debugger::ProgramState &state) {
|
|
std::string objGroup = objTable_.getObjectGroup(req.objectId);
|
|
auto scopePtr = objTable_.getScope(req.objectId);
|
|
auto valuePtr = objTable_.getValue(req.objectId);
|
|
|
|
if (scopePtr != nullptr) {
|
|
resp->result = makePropsFromScope(*scopePtr, objGroup, state);
|
|
} else if (valuePtr != nullptr) {
|
|
resp->result = makePropsFromValue(
|
|
*valuePtr, objGroup, req.ownProperties.value_or(true));
|
|
}
|
|
})
|
|
.via(executor_.get())
|
|
.thenValue([this, resp](auto &&) { sendResponseToClient(*resp); })
|
|
.thenError<std::exception>(sendErrorToClient(req.id));
|
|
}
|
|
|
|
void Connection::Impl::handle(
|
|
const m::runtime::RunIfWaitingForDebuggerRequest &req) {
|
|
if (inspector_->isAwaitingDebuggerOnStart()) {
|
|
sendResponseToClientViaExecutor(inspector_->resume(), req.id);
|
|
} else {
|
|
// We weren't awaiting a debugger. Just send an 'ok'.
|
|
sendResponseToClientViaExecutor(req.id);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Send-to-client methods
|
|
*/
|
|
|
|
void Connection::Impl::sendToClient(const std::string &str) {
|
|
if (remoteConn_) {
|
|
remoteConn_->onMessage(str);
|
|
}
|
|
}
|
|
|
|
void Connection::Impl::sendResponseToClient(const m::Response &resp) {
|
|
sendToClient(resp.toJson());
|
|
}
|
|
|
|
void Connection::Impl::sendNotificationToClient(const m::Notification ¬e) {
|
|
sendToClient(note.toJson());
|
|
}
|
|
|
|
folly::Function<void(const std::exception &)>
|
|
Connection::Impl::sendErrorToClient(int id) {
|
|
return [this, id](const std::exception &e) {
|
|
sendResponseToClient(
|
|
m::makeErrorResponse(id, m::ErrorCode::ServerError, e.what()));
|
|
};
|
|
}
|
|
|
|
void Connection::Impl::sendResponseToClientViaExecutor(int id) {
|
|
sendResponseToClientViaExecutor(folly::makeFuture(), id);
|
|
}
|
|
|
|
void Connection::Impl::sendResponseToClientViaExecutor(
|
|
const m::Response &resp) {
|
|
std::string json = resp.toJson();
|
|
|
|
folly::makeFuture()
|
|
.via(executor_.get())
|
|
.thenValue([this, json](const Unit &unit) { sendToClient(json); });
|
|
}
|
|
|
|
void Connection::Impl::sendResponseToClientViaExecutor(
|
|
folly::Future<Unit> future,
|
|
int id) {
|
|
future.via(executor_.get())
|
|
.thenValue([this, id](const Unit &unit) {
|
|
sendResponseToClient(m::makeOkResponse(id));
|
|
})
|
|
.thenError<std::exception>(sendErrorToClient(id));
|
|
}
|
|
|
|
void Connection::Impl::sendErrorToClientViaExecutor(
|
|
int id,
|
|
const std::string &error) {
|
|
folly::makeFuture()
|
|
.via(executor_.get())
|
|
.thenValue([this, id, error](const Unit &unit) {
|
|
sendResponseToClient(
|
|
makeErrorResponse(id, m::ErrorCode::ServerError, error));
|
|
});
|
|
}
|
|
|
|
void Connection::Impl::sendNotificationToClientViaExecutor(
|
|
const m::Notification ¬e) {
|
|
executor_->add(
|
|
[this, noteJson = note.toJson()]() { sendToClient(noteJson); });
|
|
}
|
|
|
|
/*
|
|
* Connection
|
|
*/
|
|
Connection::Connection(
|
|
std::unique_ptr<RuntimeAdapter> adapter,
|
|
const std::string &title,
|
|
bool waitForDebugger)
|
|
: impl_(
|
|
std::make_unique<Impl>(std::move(adapter), title, waitForDebugger)) {}
|
|
|
|
Connection::~Connection() = default;
|
|
|
|
jsi::Runtime &Connection::getRuntime() {
|
|
return impl_->getRuntime();
|
|
}
|
|
|
|
std::string Connection::getTitle() const {
|
|
return impl_->getTitle();
|
|
}
|
|
|
|
bool Connection::connect(std::unique_ptr<IRemoteConnection> remoteConn) {
|
|
return impl_->connect(std::move(remoteConn));
|
|
}
|
|
|
|
bool Connection::disconnect() {
|
|
return impl_->disconnect();
|
|
}
|
|
|
|
void Connection::sendMessage(std::string str) {
|
|
impl_->sendMessage(std::move(str));
|
|
}
|
|
|
|
} // namespace chrome
|
|
} // namespace inspector
|
|
} // namespace hermes
|
|
} // namespace facebook
|