From 7579ed3f6f1a290eb9c6380bb82015f159362b02 Mon Sep 17 00:00:00 2001 From: Riley Dulin Date: Fri, 30 Apr 2021 13:42:43 -0700 Subject: [PATCH] Implement HeapProfiler.getObjectByHeapObjectId Summary: Implement the API for querying the properties of an object found in a heap snapshot. Now when you are debugging and take a heap snapshot, you can hover over an object and inspect it! Only works for subclasses of JSObject. Doesn't work for stuff like HiddenClass, PropertyAccessor, native objects like WeakValueMap, etc. Those internal objects display "Preview is not available" which matches what Chrome prints for its own internal stuff. Changelog: [Internal] Reviewed By: avp Differential Revision: D27834672 fbshipit-source-id: 607a8984b5a48b76c5ae57f9bd5bf53168f3ec3f --- .../hermes/inspector/chrome/Connection.cpp | 74 +++++++++++++ .../hermes/inspector/chrome/MessageTypes.cpp | 101 +++++++++++++++++- .../hermes/inspector/chrome/MessageTypes.h | 50 ++++++++- .../chrome/tests/ConnectionTests.cpp | 81 ++++++++++++++ .../hermes/inspector/tools/message_types.txt | 2 + 5 files changed, 306 insertions(+), 2 deletions(-) diff --git a/ReactCommon/hermes/inspector/chrome/Connection.cpp b/ReactCommon/hermes/inspector/chrome/Connection.cpp index 00cb888156e..572a07cc282 100644 --- a/ReactCommon/hermes/inspector/chrome/Connection.cpp +++ b/ReactCommon/hermes/inspector/chrome/Connection.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -96,6 +97,9 @@ class Connection::Impl : public inspector::InspectorObserver, 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::EvaluateRequest &req) override; void handle(const m::runtime::GetPropertiesRequest &req) override; void handle(const m::runtime::RunIfWaitingForDebuggerRequest &req) override; @@ -637,6 +641,76 @@ void Connection::Impl::handle( .thenError(sendErrorToClient(req.id)); } +void Connection::Impl::handle( + const m::heapProfiler::GetObjectByHeapObjectIdRequest &req) { + uint64_t objID = atoi(req.objectId.c_str()); + folly::Optional group = req.objectGroup; + auto remoteObjPtr = std::make_shared(); + + inspector_ + ->executeIfEnabled( + "HeapProfiler.getObjectByHeapObjectId", + [this, remoteObjPtr, objID, group](const debugger::ProgramState &) { + jsi::Runtime *rt = &getRuntime(); + if (auto *hermesRT = dynamic_cast(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(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 snapshotID = std::make_shared(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(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(sendErrorToClient(req.id)); +} + void Connection::Impl::handle(const m::runtime::EvaluateRequest &req) { auto remoteObjPtr = std::make_shared(); diff --git a/ReactCommon/hermes/inspector/chrome/MessageTypes.cpp b/ReactCommon/hermes/inspector/chrome/MessageTypes.cpp index c8aea1ba453..484719e130e 100644 --- a/ReactCommon/hermes/inspector/chrome/MessageTypes.cpp +++ b/ReactCommon/hermes/inspector/chrome/MessageTypes.cpp @@ -1,5 +1,5 @@ // Copyright 2004-present Facebook. All Rights Reserved. -// @generated SignedSource<> +// @generated SignedSource<<522f29c54f207a4f7b5c33af07cf64d0>> #include "MessageTypes.h" @@ -46,6 +46,10 @@ std::unique_ptr Request::fromJsonThrowOnError(const std::string &str) { {"Debugger.stepOver", makeUnique}, {"HeapProfiler.collectGarbage", makeUnique}, + {"HeapProfiler.getHeapObjectId", + makeUnique}, + {"HeapProfiler.getObjectByHeapObjectId", + makeUnique}, {"HeapProfiler.startSampling", makeUnique}, {"HeapProfiler.startTrackingHeapObjects", @@ -730,6 +734,65 @@ void heapProfiler::CollectGarbageRequest::accept( handler.handle(*this); } +heapProfiler::GetHeapObjectIdRequest::GetHeapObjectIdRequest() + : Request("HeapProfiler.getHeapObjectId") {} + +heapProfiler::GetHeapObjectIdRequest::GetHeapObjectIdRequest(const dynamic &obj) + : Request("HeapProfiler.getHeapObjectId") { + assign(id, obj, "id"); + assign(method, obj, "method"); + + dynamic params = obj.at("params"); + assign(objectId, params, "objectId"); +} + +dynamic heapProfiler::GetHeapObjectIdRequest::toDynamic() const { + dynamic params = dynamic::object; + put(params, "objectId", objectId); + + dynamic obj = dynamic::object; + put(obj, "id", id); + put(obj, "method", method); + put(obj, "params", std::move(params)); + return obj; +} + +void heapProfiler::GetHeapObjectIdRequest::accept( + RequestHandler &handler) const { + handler.handle(*this); +} + +heapProfiler::GetObjectByHeapObjectIdRequest::GetObjectByHeapObjectIdRequest() + : Request("HeapProfiler.getObjectByHeapObjectId") {} + +heapProfiler::GetObjectByHeapObjectIdRequest::GetObjectByHeapObjectIdRequest( + const dynamic &obj) + : Request("HeapProfiler.getObjectByHeapObjectId") { + assign(id, obj, "id"); + assign(method, obj, "method"); + + dynamic params = obj.at("params"); + assign(objectId, params, "objectId"); + assign(objectGroup, params, "objectGroup"); +} + +dynamic heapProfiler::GetObjectByHeapObjectIdRequest::toDynamic() const { + dynamic params = dynamic::object; + put(params, "objectId", objectId); + put(params, "objectGroup", objectGroup); + + dynamic obj = dynamic::object; + put(obj, "id", id); + put(obj, "method", method); + put(obj, "params", std::move(params)); + return obj; +} + +void heapProfiler::GetObjectByHeapObjectIdRequest::accept( + RequestHandler &handler) const { + handler.handle(*this); +} + heapProfiler::StartSamplingRequest::StartSamplingRequest() : Request("HeapProfiler.startSampling") {} @@ -1071,6 +1134,42 @@ dynamic debugger::SetInstrumentationBreakpointResponse::toDynamic() const { return obj; } +heapProfiler::GetHeapObjectIdResponse::GetHeapObjectIdResponse( + const dynamic &obj) { + assign(id, obj, "id"); + + dynamic res = obj.at("result"); + assign(heapSnapshotObjectId, res, "heapSnapshotObjectId"); +} + +dynamic heapProfiler::GetHeapObjectIdResponse::toDynamic() const { + dynamic res = dynamic::object; + put(res, "heapSnapshotObjectId", heapSnapshotObjectId); + + dynamic obj = dynamic::object; + put(obj, "id", id); + put(obj, "result", std::move(res)); + return obj; +} + +heapProfiler::GetObjectByHeapObjectIdResponse::GetObjectByHeapObjectIdResponse( + const dynamic &obj) { + assign(id, obj, "id"); + + dynamic res = obj.at("result"); + assign(result, res, "result"); +} + +dynamic heapProfiler::GetObjectByHeapObjectIdResponse::toDynamic() const { + dynamic res = dynamic::object; + put(res, "result", result); + + dynamic obj = dynamic::object; + put(obj, "id", id); + put(obj, "result", std::move(res)); + return obj; +} + heapProfiler::StopSamplingResponse::StopSamplingResponse(const dynamic &obj) { assign(id, obj, "id"); diff --git a/ReactCommon/hermes/inspector/chrome/MessageTypes.h b/ReactCommon/hermes/inspector/chrome/MessageTypes.h index c7092f920de..2184a223588 100644 --- a/ReactCommon/hermes/inspector/chrome/MessageTypes.h +++ b/ReactCommon/hermes/inspector/chrome/MessageTypes.h @@ -1,5 +1,5 @@ // Copyright 2004-present Facebook. All Rights Reserved. -// @generated SignedSource<<0961e921eb7c5201466836c8ce82de73>> +// @generated SignedSource<> #pragma once @@ -72,6 +72,11 @@ using UnserializableValue = std::string; namespace heapProfiler { struct AddHeapSnapshotChunkNotification; struct CollectGarbageRequest; +struct GetHeapObjectIdRequest; +struct GetHeapObjectIdResponse; +struct GetObjectByHeapObjectIdRequest; +struct GetObjectByHeapObjectIdResponse; +using HeapSnapshotObjectId = std::string; struct HeapStatsUpdateNotification; struct LastSeenObjectIdNotification; struct ReportHeapSnapshotProgressNotification; @@ -107,6 +112,9 @@ struct RequestHandler { virtual void handle(const debugger::StepOutRequest &req) = 0; virtual void handle(const debugger::StepOverRequest &req) = 0; virtual void handle(const heapProfiler::CollectGarbageRequest &req) = 0; + virtual void handle(const heapProfiler::GetHeapObjectIdRequest &req) = 0; + virtual void handle( + const heapProfiler::GetObjectByHeapObjectIdRequest &req) = 0; virtual void handle(const heapProfiler::StartSamplingRequest &req) = 0; virtual void handle( const heapProfiler::StartTrackingHeapObjectsRequest &req) = 0; @@ -138,6 +146,9 @@ struct NoopRequestHandler : public RequestHandler { void handle(const debugger::StepOutRequest &req) override {} void handle(const debugger::StepOverRequest &req) override {} void handle(const heapProfiler::CollectGarbageRequest &req) override {} + void handle(const heapProfiler::GetHeapObjectIdRequest &req) override {} + void handle( + const heapProfiler::GetObjectByHeapObjectIdRequest &req) override {} void handle(const heapProfiler::StartSamplingRequest &req) override {} void handle( const heapProfiler::StartTrackingHeapObjectsRequest &req) override {} @@ -464,6 +475,27 @@ struct heapProfiler::CollectGarbageRequest : public Request { void accept(RequestHandler &handler) const override; }; +struct heapProfiler::GetHeapObjectIdRequest : public Request { + GetHeapObjectIdRequest(); + explicit GetHeapObjectIdRequest(const folly::dynamic &obj); + + folly::dynamic toDynamic() const override; + void accept(RequestHandler &handler) const override; + + runtime::RemoteObjectId objectId{}; +}; + +struct heapProfiler::GetObjectByHeapObjectIdRequest : public Request { + GetObjectByHeapObjectIdRequest(); + explicit GetObjectByHeapObjectIdRequest(const folly::dynamic &obj); + + folly::dynamic toDynamic() const override; + void accept(RequestHandler &handler) const override; + + heapProfiler::HeapSnapshotObjectId objectId{}; + folly::Optional objectGroup; +}; + struct heapProfiler::StartSamplingRequest : public Request { StartSamplingRequest(); explicit StartSamplingRequest(const folly::dynamic &obj); @@ -602,6 +634,22 @@ struct debugger::SetInstrumentationBreakpointResponse : public Response { debugger::BreakpointId breakpointId{}; }; +struct heapProfiler::GetHeapObjectIdResponse : public Response { + GetHeapObjectIdResponse() = default; + explicit GetHeapObjectIdResponse(const folly::dynamic &obj); + folly::dynamic toDynamic() const override; + + heapProfiler::HeapSnapshotObjectId heapSnapshotObjectId{}; +}; + +struct heapProfiler::GetObjectByHeapObjectIdResponse : public Response { + GetObjectByHeapObjectIdResponse() = default; + explicit GetObjectByHeapObjectIdResponse(const folly::dynamic &obj); + folly::dynamic toDynamic() const override; + + runtime::RemoteObject result{}; +}; + struct heapProfiler::StopSamplingResponse : public Response { StopSamplingResponse() = default; explicit StopSamplingResponse(const folly::dynamic &obj); diff --git a/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp b/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp index 952e84aac46..f579799bbae 100644 --- a/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp +++ b/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp @@ -22,6 +22,7 @@ #include #include #include +#include namespace facebook { namespace hermes { @@ -2512,6 +2513,86 @@ TEST(ConnectionTests, heapProfilerSampling) { expectNotification(conn); } +TEST(ConnectionTests, heapSnapshotRemoteObject) { + TestContext context; + AsyncHermesRuntime &asyncRuntime = context.runtime(); + std::shared_ptr runtime = asyncRuntime.runtime(); + SyncConnection &conn = context.conn(); + int msgId = 1; + + send(conn, msgId++); + expectExecutionContextCreated(conn); + + asyncRuntime.executeScriptAsync(R"( + storeValue([1, 2, 3]); + debugger; + )"); + expectNotification(conn); + + // We should get a pause before the first statement. + expectNotification(conn); + + { + // Take a heap snapshot first to assign IDs. + m::heapProfiler::TakeHeapSnapshotRequest req; + req.id = msgId++; + req.reportProgress = false; + // We don't need the response because we can directly query for object IDs + // from the runtime. + send(conn, req); + } + + const uint64_t globalObjID = runtime->getUniqueID(runtime->global()); + jsi::Value storedValue = asyncRuntime.awaitStoredValue(); + const uint64_t storedObjID = + runtime->getUniqueID(storedValue.asObject(*runtime)); + + auto testObject = [&msgId, &conn]( + uint64_t objID, + const char *type, + const char *className, + const char *description, + const char *subtype) { + // Get the object by its snapshot ID. + m::heapProfiler::GetObjectByHeapObjectIdRequest req; + req.id = msgId++; + req.objectId = std::to_string(objID); + auto resp = send< + m::heapProfiler::GetObjectByHeapObjectIdRequest, + m::heapProfiler::GetObjectByHeapObjectIdResponse>(conn, req); + EXPECT_EQ(resp.result.type, type); + EXPECT_EQ(resp.result.className, className); + EXPECT_EQ(resp.result.description, description); + if (subtype) { + EXPECT_EQ(resp.result.subtype, subtype); + } + + // Check that fetching the object by heap snapshot ID works. + m::heapProfiler::GetHeapObjectIdRequest idReq; + idReq.id = msgId++; + idReq.objectId = resp.result.objectId.value(); + auto idResp = send< + m::heapProfiler::GetHeapObjectIdRequest, + m::heapProfiler::GetHeapObjectIdResponse>(conn, idReq); + EXPECT_EQ(atoi(idResp.heapSnapshotObjectId.c_str()), objID); + }; + + // Test once before a collection. + testObject(globalObjID, "object", "Object", "Object", nullptr); + testObject(storedObjID, "object", "Array", "Array(3)", "array"); + // Force a collection to move the heap. + runtime->instrumentation().collectGarbage("test"); + // A collection should not disturb the unique ID lookup, and it should be the + // same object as before. Note that it won't have the same remote ID, because + // Hermes doesn't do uniquing. + testObject(globalObjID, "object", "Object", "Object", nullptr); + testObject(storedObjID, "object", "Array", "Array(3)", "array"); + + // Resume and exit. + send(conn, msgId++); + expectNotification(conn); +} + } // namespace chrome } // namespace inspector } // namespace hermes diff --git a/ReactCommon/hermes/inspector/tools/message_types.txt b/ReactCommon/hermes/inspector/tools/message_types.txt index 155c78ae776..6267e82ee0e 100644 --- a/ReactCommon/hermes/inspector/tools/message_types.txt +++ b/ReactCommon/hermes/inspector/tools/message_types.txt @@ -26,6 +26,8 @@ HeapProfiler.startSampling HeapProfiler.stopSampling HeapProfiler.heapStatsUpdate HeapProfiler.lastSeenObjectId +HeapProfiler.getObjectByHeapObjectId +HeapProfiler.getHeapObjectId Runtime.consoleAPICalled Runtime.evaluate Runtime.executionContextCreated