diff --git a/ReactCommon/hermes/inspector/chrome/Connection.cpp b/ReactCommon/hermes/inspector/chrome/Connection.cpp index e554f654dcb..b7b264093b2 100644 --- a/ReactCommon/hermes/inspector/chrome/Connection.cpp +++ b/ReactCommon/hermes/inspector/chrome/Connection.cpp @@ -101,6 +101,8 @@ class Connection::Impl : public inspector::InspectorObserver, void handle( const m::heapProfiler::GetObjectByHeapObjectIdRequest &req) override; void handle(const m::heapProfiler::GetHeapObjectIdRequest &req) override; + void handle(const m::profiler::StartRequest &req) override; + void handle(const m::profiler::StopRequest &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; @@ -135,6 +137,11 @@ class Connection::Impl : public inspector::InspectorObserver, void sendNotificationToClientViaExecutor(const m::Notification ¬e); void sendErrorToClientViaExecutor(int id, const std::string &error); + template + void runInExecutor(int id, C callback) { + folly::via(executor_.get(), [cb = std::move(callback)]() { cb(); }); + } + std::shared_ptr runtimeAdapter_; std::string title_; @@ -721,6 +728,60 @@ void Connection::Impl::handle( .thenError(sendErrorToClient(req.id)); } +void Connection::Impl::handle(const m::profiler::StartRequest &req) { + auto *hermesRT = dynamic_cast(&getRuntime()); + + if (!hermesRT) { + sendResponseToClientViaExecutor(m::makeErrorResponse( + req.id, m::ErrorCode::ServerError, "Unhandled Runtime kind.")); + return; + } + + runInExecutor(req.id, [this, id = req.id]() { + HermesRuntime::enableSamplingProfiler(); + sendResponseToClient(m::makeOkResponse(id)); + }); +} + +void Connection::Impl::handle(const m::profiler::StopRequest &req) { + auto *hermesRT = dynamic_cast(&getRuntime()); + + if (!hermesRT) { + sendResponseToClientViaExecutor(m::makeErrorResponse( + req.id, m::ErrorCode::ServerError, "Unhandled Runtime kind.")); + return; + } + + runInExecutor(req.id, [this, id = req.id, hermesRT]() { + HermesRuntime::disableSamplingProfiler(); + + std::ostringstream profileStream; + // HermesRuntime instance methods are usually unsafe to be called with a + // running VM, but sampledTraceToStreamInDevToolsFormat is an exception to + // that rule -- it synchronizes access to shared resources so it can be + // safely invoked with a running VM. + hermesRT->sampledTraceToStreamInDevToolsFormat(profileStream); + + // Hermes can emit the proper format directly, but it still needs to + // be parsed into a dynamic. + try { + m::profiler::StopResponse resp; + resp.id = id; + // parseJson throws on errors, so make sure we don't crash the app + // if somehow the sampling profiler output is borked. + resp.profile = m::profiler::Profile( + folly::parseJson(std::move(profileStream).str())); + sendResponseToClient(resp); + } catch (const std::exception &) { + LOG(ERROR) << "Failed to parse Sampling Profiler output"; + sendResponseToClient(m::makeErrorResponse( + id, + m::ErrorCode::InternalError, + "Hermes profile output could not be parsed.")); + } + }); +} + namespace { /// Runtime.CallArguments can have their values specified "inline", or they can /// have remote references. The inline values are eval'd together with the diff --git a/ReactCommon/hermes/inspector/chrome/MessageTypes.cpp b/ReactCommon/hermes/inspector/chrome/MessageTypes.cpp index 9e278ae531c..551874682ba 100644 --- a/ReactCommon/hermes/inspector/chrome/MessageTypes.cpp +++ b/ReactCommon/hermes/inspector/chrome/MessageTypes.cpp @@ -1,5 +1,5 @@ // Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved. -// @generated SignedSource<<130ce3da2ad67004eb7bb91f19028a89>> +// @generated SignedSource<> #include "MessageTypes.h" @@ -60,6 +60,8 @@ std::unique_ptr Request::fromJsonThrowOnError(const std::string &str) { makeUnique}, {"HeapProfiler.takeHeapSnapshot", makeUnique}, + {"Profiler.start", makeUnique}, + {"Profiler.stop", makeUnique}, {"Runtime.callFunctionOn", makeUnique}, {"Runtime.evaluate", makeUnique}, {"Runtime.getHeapUsage", makeUnique}, @@ -276,6 +278,59 @@ dynamic heapProfiler::SamplingHeapProfile::toDynamic() const { return obj; } +profiler::PositionTickInfo::PositionTickInfo(const dynamic &obj) { + assign(line, obj, "line"); + assign(ticks, obj, "ticks"); +} + +dynamic profiler::PositionTickInfo::toDynamic() const { + dynamic obj = dynamic::object; + + put(obj, "line", line); + put(obj, "ticks", ticks); + return obj; +} + +profiler::ProfileNode::ProfileNode(const dynamic &obj) { + assign(id, obj, "id"); + assign(callFrame, obj, "callFrame"); + assign(hitCount, obj, "hitCount"); + assign(children, obj, "children"); + assign(deoptReason, obj, "deoptReason"); + assign(positionTicks, obj, "positionTicks"); +} + +dynamic profiler::ProfileNode::toDynamic() const { + dynamic obj = dynamic::object; + + put(obj, "id", id); + put(obj, "callFrame", callFrame); + put(obj, "hitCount", hitCount); + put(obj, "children", children); + put(obj, "deoptReason", deoptReason); + put(obj, "positionTicks", positionTicks); + return obj; +} + +profiler::Profile::Profile(const dynamic &obj) { + assign(nodes, obj, "nodes"); + assign(startTime, obj, "startTime"); + assign(endTime, obj, "endTime"); + assign(samples, obj, "samples"); + assign(timeDeltas, obj, "timeDeltas"); +} + +dynamic profiler::Profile::toDynamic() const { + dynamic obj = dynamic::object; + + put(obj, "nodes", nodes); + put(obj, "startTime", startTime); + put(obj, "endTime", endTime); + put(obj, "samples", samples); + put(obj, "timeDeltas", timeDeltas); + return obj; +} + runtime::CallArgument::CallArgument(const dynamic &obj) { assign(value, obj, "value"); assign(unserializableValue, obj, "unserializableValue"); @@ -974,6 +1029,44 @@ void heapProfiler::TakeHeapSnapshotRequest::accept( handler.handle(*this); } +profiler::StartRequest::StartRequest() : Request("Profiler.start") {} + +profiler::StartRequest::StartRequest(const dynamic &obj) + : Request("Profiler.start") { + assign(id, obj, "id"); + assign(method, obj, "method"); +} + +dynamic profiler::StartRequest::toDynamic() const { + dynamic obj = dynamic::object; + put(obj, "id", id); + put(obj, "method", method); + return obj; +} + +void profiler::StartRequest::accept(RequestHandler &handler) const { + handler.handle(*this); +} + +profiler::StopRequest::StopRequest() : Request("Profiler.stop") {} + +profiler::StopRequest::StopRequest(const dynamic &obj) + : Request("Profiler.stop") { + assign(id, obj, "id"); + assign(method, obj, "method"); +} + +dynamic profiler::StopRequest::toDynamic() const { + dynamic obj = dynamic::object; + put(obj, "id", id); + put(obj, "method", method); + return obj; +} + +void profiler::StopRequest::accept(RequestHandler &handler) const { + handler.handle(*this); +} + runtime::CallFunctionOnRequest::CallFunctionOnRequest() : Request("Runtime.callFunctionOn") {} @@ -1293,6 +1386,23 @@ dynamic heapProfiler::StopSamplingResponse::toDynamic() const { return obj; } +profiler::StopResponse::StopResponse(const dynamic &obj) { + assign(id, obj, "id"); + + dynamic res = obj.at("result"); + assign(profile, res, "profile"); +} + +dynamic profiler::StopResponse::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::CallFunctionOnResponse::CallFunctionOnResponse(const dynamic &obj) { assign(id, obj, "id"); diff --git a/ReactCommon/hermes/inspector/chrome/MessageTypes.h b/ReactCommon/hermes/inspector/chrome/MessageTypes.h index 5979d2f6889..c74a21d357e 100644 --- a/ReactCommon/hermes/inspector/chrome/MessageTypes.h +++ b/ReactCommon/hermes/inspector/chrome/MessageTypes.h @@ -1,5 +1,5 @@ // Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved. -// @generated SignedSource<<41786947d74eb3f47d8168b82b5ccf85>> +// @generated SignedSource<> #pragma once @@ -96,6 +96,15 @@ struct StopTrackingHeapObjectsRequest; struct TakeHeapSnapshotRequest; } // namespace heapProfiler +namespace profiler { +struct PositionTickInfo; +struct Profile; +struct ProfileNode; +struct StartRequest; +struct StopRequest; +struct StopResponse; +} // namespace profiler + /// RequestHandler handles requests via the visitor pattern. struct RequestHandler { virtual ~RequestHandler() = default; @@ -127,6 +136,8 @@ struct RequestHandler { virtual void handle( const heapProfiler::StopTrackingHeapObjectsRequest &req) = 0; virtual void handle(const heapProfiler::TakeHeapSnapshotRequest &req) = 0; + virtual void handle(const profiler::StartRequest &req) = 0; + virtual void handle(const profiler::StopRequest &req) = 0; virtual void handle(const runtime::CallFunctionOnRequest &req) = 0; virtual void handle(const runtime::EvaluateRequest &req) = 0; virtual void handle(const runtime::GetHeapUsageRequest &req) = 0; @@ -163,6 +174,8 @@ struct NoopRequestHandler : public RequestHandler { void handle( const heapProfiler::StopTrackingHeapObjectsRequest &req) override {} void handle(const heapProfiler::TakeHeapSnapshotRequest &req) override {} + void handle(const profiler::StartRequest &req) override {} + void handle(const profiler::StopRequest &req) override {} void handle(const runtime::CallFunctionOnRequest &req) override {} void handle(const runtime::EvaluateRequest &req) override {} void handle(const runtime::GetHeapUsageRequest &req) override {} @@ -290,6 +303,40 @@ struct heapProfiler::SamplingHeapProfile : public Serializable { std::vector samples; }; +struct profiler::PositionTickInfo : public Serializable { + PositionTickInfo() = default; + explicit PositionTickInfo(const folly::dynamic &obj); + folly::dynamic toDynamic() const override; + + int line{}; + int ticks{}; +}; + +struct profiler::ProfileNode : public Serializable { + ProfileNode() = default; + explicit ProfileNode(const folly::dynamic &obj); + folly::dynamic toDynamic() const override; + + int id{}; + runtime::CallFrame callFrame{}; + folly::Optional hitCount; + folly::Optional> children; + folly::Optional deoptReason; + folly::Optional> positionTicks; +}; + +struct profiler::Profile : public Serializable { + Profile() = default; + explicit Profile(const folly::dynamic &obj); + folly::dynamic toDynamic() const override; + + std::vector nodes; + double startTime{}; + double endTime{}; + folly::Optional> samples; + folly::Optional> timeDeltas; +}; + struct runtime::CallArgument : public Serializable { CallArgument() = default; explicit CallArgument(const folly::dynamic &obj); @@ -569,6 +616,22 @@ struct heapProfiler::TakeHeapSnapshotRequest : public Request { folly::Optional captureNumericValue; }; +struct profiler::StartRequest : public Request { + StartRequest(); + explicit StartRequest(const folly::dynamic &obj); + + folly::dynamic toDynamic() const override; + void accept(RequestHandler &handler) const override; +}; + +struct profiler::StopRequest : public Request { + StopRequest(); + explicit StopRequest(const folly::dynamic &obj); + + folly::dynamic toDynamic() const override; + void accept(RequestHandler &handler) const override; +}; + struct runtime::CallFunctionOnRequest : public Request { CallFunctionOnRequest(); explicit CallFunctionOnRequest(const folly::dynamic &obj); @@ -707,6 +770,14 @@ struct heapProfiler::StopSamplingResponse : public Response { heapProfiler::SamplingHeapProfile profile{}; }; +struct profiler::StopResponse : public Response { + StopResponse() = default; + explicit StopResponse(const folly::dynamic &obj); + folly::dynamic toDynamic() const override; + + profiler::Profile profile{}; +}; + struct runtime::CallFunctionOnResponse : public Response { CallFunctionOnResponse() = default; explicit CallFunctionOnResponse(const folly::dynamic &obj); diff --git a/ReactCommon/hermes/inspector/chrome/tests/AsyncHermesRuntime.cpp b/ReactCommon/hermes/inspector/chrome/tests/AsyncHermesRuntime.cpp index ae490a9553a..9c630ad0121 100644 --- a/ReactCommon/hermes/inspector/chrome/tests/AsyncHermesRuntime.cpp +++ b/ReactCommon/hermes/inspector/chrome/tests/AsyncHermesRuntime.cpp @@ -139,6 +139,27 @@ std::string AsyncHermesRuntime::getLastThrownExceptionMessage() { return thrownExceptions_.back(); } +void AsyncHermesRuntime::registerForProfilingInExecutor() { + // Sampling profiler registration needs to happen in the thread where JS runs. + folly::via(executor_.get(), [runtime = runtime_]() { + runtime->registerForProfiling(); + }); + + // Wait until the executor is registered for profiling. + wait(); +} + +void AsyncHermesRuntime::unregisterForProfilingInExecutor() { + // Sampling profiler deregistration needs to happen in the thread where JS + // runs. + folly::via(executor_.get(), [runtime = runtime_]() { + runtime->unregisterForProfiling(); + }); + + // Wait until the executor is unregistered for profiling. + wait(); +} + } // namespace chrome } // namespace inspector } // namespace hermes diff --git a/ReactCommon/hermes/inspector/chrome/tests/AsyncHermesRuntime.h b/ReactCommon/hermes/inspector/chrome/tests/AsyncHermesRuntime.h index 645e43726bb..8772b14b62f 100644 --- a/ReactCommon/hermes/inspector/chrome/tests/AsyncHermesRuntime.h +++ b/ReactCommon/hermes/inspector/chrome/tests/AsyncHermesRuntime.h @@ -95,6 +95,16 @@ class AsyncHermesRuntime { */ std::string getLastThrownExceptionMessage(); + /** + * registers the runtime for profiling in the executor thread. + */ + void registerForProfilingInExecutor(); + + /** + * unregisters the runtime for profiling in the executor thread. + */ + void unregisterForProfilingInExecutor(); + private: jsi::Value shouldStop( jsi::Runtime &runtime, @@ -115,6 +125,23 @@ class AsyncHermesRuntime { std::vector thrownExceptions_; }; +/// RAII-style class dealing with sampling profiler registration in tests. This +/// is especially important in tests -- if any test failure is caused by an +/// uncaught exception, stack unwinding will destroy a VM registered for +/// profiling in a thread that's not the one where registration happened, which +/// will lead to a hermes fatal error. Using this RAII class ensure that the +/// proper test failure cause is reported. +struct SamplingProfilerRAII { + explicit SamplingProfilerRAII(AsyncHermesRuntime &rt) : runtime_(rt) { + runtime_.registerForProfilingInExecutor(); + } + + ~SamplingProfilerRAII() { + runtime_.unregisterForProfilingInExecutor(); + } + + AsyncHermesRuntime &runtime_; +}; } // namespace chrome } // namespace inspector } // namespace hermes diff --git a/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp b/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp index 502b1f10f9d..b6538ea56e6 100644 --- a/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp +++ b/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp @@ -2837,6 +2837,53 @@ TEST(ConnectionTests, heapSnapshotRemoteObject) { expectNotification(conn); } +TEST(ConnectionTests, testBasicProfilerOperation) { + TestContext context; + AsyncHermesRuntime &asyncRuntime = context.runtime(); + SamplingProfilerRAII spRegistration(asyncRuntime); + SyncConnection &conn = context.conn(); + int msgId = 1; + + asyncRuntime.executeScriptAsync(R"( + while(!shouldStop()); + )"); + + send(conn, msgId++); + expectExecutionContextCreated(conn); + expectNotification(conn); + + // Start the sampling profiler. At this point it is not safe to manipulate the + // VM, so... + send(conn, msgId++); + + // Disable the debugger. + send(conn, msgId++); + + // Keep the profiler running for a small amount of time to allow for some + // samples to be collected. + std::this_thread::sleep_for(500ms); + + // Finally, re-enable the debugger in order to stop profiling. + send(conn, msgId++); + expectExecutionContextCreated(conn); + + // Being re-attached to the VM, send the stop sampling profile request. + { + auto resp = send( + conn, msgId++); + + const m::profiler::Profile &profile = resp.profile; + EXPECT_GT(profile.nodes.size(), 0); + EXPECT_LT(profile.startTime, profile.endTime); + ASSERT_TRUE(profile.samples); + EXPECT_FALSE(profile.samples->empty()); + ASSERT_TRUE(profile.timeDeltas); + EXPECT_EQ(profile.samples->size(), profile.timeDeltas->size()); + } + + asyncRuntime.stop(); +} + } // 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 d2868e6ab3c..801ff6a49dc 100644 --- a/ReactCommon/hermes/inspector/tools/message_types.txt +++ b/ReactCommon/hermes/inspector/tools/message_types.txt @@ -28,6 +28,8 @@ HeapProfiler.heapStatsUpdate HeapProfiler.lastSeenObjectId HeapProfiler.getObjectByHeapObjectId HeapProfiler.getHeapObjectId +Profiler.start +Profiler.stop Runtime.callFunctionOn Runtime.consoleAPICalled Runtime.evaluate