From bcc79bd4c7c1c917e4db0728e18783eee2208129 Mon Sep 17 00:00:00 2001 From: Riley Dulin Date: Thu, 18 Mar 2021 10:38:11 -0700 Subject: [PATCH] Add support for the sampling memory profiler to the Chrome inspector Summary: In the Chrome inspector, add support for `HeapProfiler.startSampling` and `HeapProfiler.stopSampling`. These two protocols turn the sampling heap profiler on and off, and write the results on the socket. Then Chrome's visualizer pieces the information together to produce a flame graph of memory usage. Added a unit test to make sure sampling can be turned on and off, and there are some samples returned. Changelog: [Internal] Reviewed By: avp Differential Revision: D26835148 fbshipit-source-id: d1be3cee791e42da5d9e117c3b8259b6622b98f4 --- .../hermes/inspector/chrome/Connection.cpp | 41 ++++++ .../hermes/inspector/chrome/MessageTypes.cpp | 117 +++++++++++++++++- .../hermes/inspector/chrome/MessageTypes.h | 68 +++++++++- .../chrome/tests/ConnectionTests.cpp | 63 ++++++++++ .../hermes/inspector/tools/message_types.txt | 2 + 5 files changed, 289 insertions(+), 2 deletions(-) diff --git a/ReactCommon/hermes/inspector/chrome/Connection.cpp b/ReactCommon/hermes/inspector/chrome/Connection.cpp index 8af32ff530f..00cb888156e 100644 --- a/ReactCommon/hermes/inspector/chrome/Connection.cpp +++ b/ReactCommon/hermes/inspector/chrome/Connection.cpp @@ -93,6 +93,8 @@ class Connection::Impl : public inspector::InspectorObserver, 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::runtime::EvaluateRequest &req) override; void handle(const m::runtime::GetPropertiesRequest &req) override; @@ -580,6 +582,45 @@ void Connection::Impl::handle( /* 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(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(sendErrorToClient(req.id)); +} + void Connection::Impl::handle( const m::heapProfiler::CollectGarbageRequest &req) { const auto id = req.id; diff --git a/ReactCommon/hermes/inspector/chrome/MessageTypes.cpp b/ReactCommon/hermes/inspector/chrome/MessageTypes.cpp index 2a71a05640d..c8aea1ba453 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<> #include "MessageTypes.h" @@ -46,8 +46,12 @@ std::unique_ptr Request::fromJsonThrowOnError(const std::string &str) { {"Debugger.stepOver", makeUnique}, {"HeapProfiler.collectGarbage", makeUnique}, + {"HeapProfiler.startSampling", + makeUnique}, {"HeapProfiler.startTrackingHeapObjects", makeUnique}, + {"HeapProfiler.stopSampling", + makeUnique}, {"HeapProfiler.stopTrackingHeapObjects", makeUnique}, {"HeapProfiler.takeHeapSnapshot", @@ -219,6 +223,53 @@ dynamic debugger::CallFrame::toDynamic() const { return obj; } +heapProfiler::SamplingHeapProfileNode::SamplingHeapProfileNode( + const dynamic &obj) { + assign(callFrame, obj, "callFrame"); + assign(selfSize, obj, "selfSize"); + assign(id, obj, "id"); + assign(children, obj, "children"); +} + +dynamic heapProfiler::SamplingHeapProfileNode::toDynamic() const { + dynamic obj = dynamic::object; + + put(obj, "callFrame", callFrame); + put(obj, "selfSize", selfSize); + put(obj, "id", id); + put(obj, "children", children); + return obj; +} + +heapProfiler::SamplingHeapProfileSample::SamplingHeapProfileSample( + const dynamic &obj) { + assign(size, obj, "size"); + assign(nodeId, obj, "nodeId"); + assign(ordinal, obj, "ordinal"); +} + +dynamic heapProfiler::SamplingHeapProfileSample::toDynamic() const { + dynamic obj = dynamic::object; + + put(obj, "size", size); + put(obj, "nodeId", nodeId); + put(obj, "ordinal", ordinal); + return obj; +} + +heapProfiler::SamplingHeapProfile::SamplingHeapProfile(const dynamic &obj) { + assign(head, obj, "head"); + assign(samples, obj, "samples"); +} + +dynamic heapProfiler::SamplingHeapProfile::toDynamic() const { + dynamic obj = dynamic::object; + + put(obj, "head", head); + put(obj, "samples", samples); + return obj; +} + runtime::ExecutionContextDescription::ExecutionContextDescription( const dynamic &obj) { assign(id, obj, "id"); @@ -679,6 +730,33 @@ void heapProfiler::CollectGarbageRequest::accept( handler.handle(*this); } +heapProfiler::StartSamplingRequest::StartSamplingRequest() + : Request("HeapProfiler.startSampling") {} + +heapProfiler::StartSamplingRequest::StartSamplingRequest(const dynamic &obj) + : Request("HeapProfiler.startSampling") { + assign(id, obj, "id"); + assign(method, obj, "method"); + + dynamic params = obj.at("params"); + assign(samplingInterval, params, "samplingInterval"); +} + +dynamic heapProfiler::StartSamplingRequest::toDynamic() const { + dynamic params = dynamic::object; + put(params, "samplingInterval", samplingInterval); + + dynamic obj = dynamic::object; + put(obj, "id", id); + put(obj, "method", method); + put(obj, "params", std::move(params)); + return obj; +} + +void heapProfiler::StartSamplingRequest::accept(RequestHandler &handler) const { + handler.handle(*this); +} + heapProfiler::StartTrackingHeapObjectsRequest::StartTrackingHeapObjectsRequest() : Request("HeapProfiler.startTrackingHeapObjects") {} @@ -708,6 +786,26 @@ void heapProfiler::StartTrackingHeapObjectsRequest::accept( handler.handle(*this); } +heapProfiler::StopSamplingRequest::StopSamplingRequest() + : Request("HeapProfiler.stopSampling") {} + +heapProfiler::StopSamplingRequest::StopSamplingRequest(const dynamic &obj) + : Request("HeapProfiler.stopSampling") { + assign(id, obj, "id"); + assign(method, obj, "method"); +} + +dynamic heapProfiler::StopSamplingRequest::toDynamic() const { + dynamic obj = dynamic::object; + put(obj, "id", id); + put(obj, "method", method); + return obj; +} + +void heapProfiler::StopSamplingRequest::accept(RequestHandler &handler) const { + handler.handle(*this); +} + heapProfiler::StopTrackingHeapObjectsRequest::StopTrackingHeapObjectsRequest() : Request("HeapProfiler.stopTrackingHeapObjects") {} @@ -973,6 +1071,23 @@ dynamic debugger::SetInstrumentationBreakpointResponse::toDynamic() const { return obj; } +heapProfiler::StopSamplingResponse::StopSamplingResponse(const dynamic &obj) { + assign(id, obj, "id"); + + dynamic res = obj.at("result"); + assign(profile, res, "profile"); +} + +dynamic heapProfiler::StopSamplingResponse::toDynamic() const { + dynamic res = dynamic::object; + put(res, "profile", profile); + + dynamic obj = dynamic::object; + put(obj, "id", id); + put(obj, "result", std::move(res)); + return obj; +} + runtime::EvaluateResponse::EvaluateResponse(const dynamic &obj) { assign(id, obj, "id"); diff --git a/ReactCommon/hermes/inspector/chrome/MessageTypes.h b/ReactCommon/hermes/inspector/chrome/MessageTypes.h index 7778d88dc36..c7092f920de 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<> +// @generated SignedSource<<0961e921eb7c5201466836c8ce82de73>> #pragma once @@ -75,7 +75,13 @@ struct CollectGarbageRequest; struct HeapStatsUpdateNotification; struct LastSeenObjectIdNotification; struct ReportHeapSnapshotProgressNotification; +struct SamplingHeapProfile; +struct SamplingHeapProfileNode; +struct SamplingHeapProfileSample; +struct StartSamplingRequest; struct StartTrackingHeapObjectsRequest; +struct StopSamplingRequest; +struct StopSamplingResponse; struct StopTrackingHeapObjectsRequest; struct TakeHeapSnapshotRequest; } // namespace heapProfiler @@ -101,8 +107,10 @@ 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::StartSamplingRequest &req) = 0; virtual void handle( const heapProfiler::StartTrackingHeapObjectsRequest &req) = 0; + virtual void handle(const heapProfiler::StopSamplingRequest &req) = 0; virtual void handle( const heapProfiler::StopTrackingHeapObjectsRequest &req) = 0; virtual void handle(const heapProfiler::TakeHeapSnapshotRequest &req) = 0; @@ -130,8 +138,10 @@ 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::StartSamplingRequest &req) override {} void handle( const heapProfiler::StartTrackingHeapObjectsRequest &req) override {} + void handle(const heapProfiler::StopSamplingRequest &req) override {} void handle( const heapProfiler::StopTrackingHeapObjectsRequest &req) override {} void handle(const heapProfiler::TakeHeapSnapshotRequest &req) override {} @@ -230,6 +240,36 @@ struct debugger::CallFrame : public Serializable { folly::Optional returnValue; }; +struct heapProfiler::SamplingHeapProfileNode : public Serializable { + SamplingHeapProfileNode() = default; + explicit SamplingHeapProfileNode(const folly::dynamic &obj); + folly::dynamic toDynamic() const override; + + runtime::CallFrame callFrame{}; + double selfSize{}; + int id{}; + std::vector children; +}; + +struct heapProfiler::SamplingHeapProfileSample : public Serializable { + SamplingHeapProfileSample() = default; + explicit SamplingHeapProfileSample(const folly::dynamic &obj); + folly::dynamic toDynamic() const override; + + double size{}; + int nodeId{}; + double ordinal{}; +}; + +struct heapProfiler::SamplingHeapProfile : public Serializable { + SamplingHeapProfile() = default; + explicit SamplingHeapProfile(const folly::dynamic &obj); + folly::dynamic toDynamic() const override; + + heapProfiler::SamplingHeapProfileNode head{}; + std::vector samples; +}; + struct runtime::ExecutionContextDescription : public Serializable { ExecutionContextDescription() = default; explicit ExecutionContextDescription(const folly::dynamic &obj); @@ -424,6 +464,16 @@ struct heapProfiler::CollectGarbageRequest : public Request { void accept(RequestHandler &handler) const override; }; +struct heapProfiler::StartSamplingRequest : public Request { + StartSamplingRequest(); + explicit StartSamplingRequest(const folly::dynamic &obj); + + folly::dynamic toDynamic() const override; + void accept(RequestHandler &handler) const override; + + folly::Optional samplingInterval; +}; + struct heapProfiler::StartTrackingHeapObjectsRequest : public Request { StartTrackingHeapObjectsRequest(); explicit StartTrackingHeapObjectsRequest(const folly::dynamic &obj); @@ -434,6 +484,14 @@ struct heapProfiler::StartTrackingHeapObjectsRequest : public Request { folly::Optional trackAllocations; }; +struct heapProfiler::StopSamplingRequest : public Request { + StopSamplingRequest(); + explicit StopSamplingRequest(const folly::dynamic &obj); + + folly::dynamic toDynamic() const override; + void accept(RequestHandler &handler) const override; +}; + struct heapProfiler::StopTrackingHeapObjectsRequest : public Request { StopTrackingHeapObjectsRequest(); explicit StopTrackingHeapObjectsRequest(const folly::dynamic &obj); @@ -544,6 +602,14 @@ struct debugger::SetInstrumentationBreakpointResponse : public Response { debugger::BreakpointId breakpointId{}; }; +struct heapProfiler::StopSamplingResponse : public Response { + StopSamplingResponse() = default; + explicit StopSamplingResponse(const folly::dynamic &obj); + folly::dynamic toDynamic() const override; + + heapProfiler::SamplingHeapProfile profile{}; +}; + struct runtime::EvaluateResponse : public Response { EvaluateResponse() = default; explicit EvaluateResponse(const folly::dynamic &obj); diff --git a/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp b/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp index e4d857aac3f..952e84aac46 100644 --- a/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp +++ b/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp @@ -199,6 +199,12 @@ ResponseType send(SyncConnection &conn, int id) { return expectResponse(conn, id); } +template +ResponseType send(SyncConnection &conn, RequestType req) { + conn.send(req.toJson()); + return expectResponse(conn, req.id); +} + void sendRuntimeEvalRequest( SyncConnection &conn, int id, @@ -2449,6 +2455,63 @@ TEST(ConnectionTests, runIfWaitingForDebugger) { expectNotification(conn); } +TEST(ConnectionTests, heapProfilerSampling) { + TestContext context; + AsyncHermesRuntime &asyncRuntime = context.runtime(); + SyncConnection &conn = context.conn(); + int msgId = 1; + + send(conn, msgId++); + expectExecutionContextCreated(conn); + + asyncRuntime.executeScriptAsync(R"( + debugger; + function allocator() { + // Do some allocation. + return new Object; + } + (function main() { + var a = []; + for (var i = 0; i < 100; i++) { + a[i] = allocator(); + } + })(); + debugger; + )"); + expectNotification(conn); + + // We should get a pause before the first statement. + expectNotification(conn); + + { + m::heapProfiler::StartSamplingRequest req; + req.id = msgId++; + // Sample every 256 bytes to ensure there are some samples. The default is + // 32768, which is too high for a small example. Note that sampling is a + // random process, so there's no guarantee there will be any samples in any + // finite number of allocations. In practice the likelihood is so high that + // there shouldn't be any issues. + req.samplingInterval = 256; + send(conn, req); + } + // Resume, run the allocations, and once it's paused again, stop them. + send(conn, msgId++); + expectNotification(conn); + expectNotification(conn); + // Send the stop sampling request, expect the value coming back to be JSON. + auto resp = send< + m::heapProfiler::StopSamplingRequest, + m::heapProfiler::StopSamplingResponse>(conn, msgId++); + // Make sure there were some samples. + EXPECT_NE(resp.profile.samples.size(), 0); + // Don't test the content of the JSON, that is tested via the + // SamplingHeapProfilerTest. + + // 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 5f009532a9f..155c78ae776 100644 --- a/ReactCommon/hermes/inspector/tools/message_types.txt +++ b/ReactCommon/hermes/inspector/tools/message_types.txt @@ -22,6 +22,8 @@ HeapProfiler.reportHeapSnapshotProgress HeapProfiler.takeHeapSnapshot HeapProfiler.startTrackingHeapObjects HeapProfiler.stopTrackingHeapObjects +HeapProfiler.startSampling +HeapProfiler.stopSampling HeapProfiler.heapStatsUpdate HeapProfiler.lastSeenObjectId Runtime.consoleAPICalled