Add support for devtools' profiler

Summary:
Add support for analyzing sampling profiler data in devtools' JavaScript Profiler tab.

Changelog: [Added]

Reviewed By: neildhar

Differential Revision: D34114709

fbshipit-source-id: 1bf02ce02a250f68af1189c6516fb79795a8e887
This commit is contained in:
John Porto
2022-04-01 23:03:55 -07:00
committed by Facebook GitHub Bot
parent 184dfb8f8b
commit fefa7b6ac8
7 changed files with 341 additions and 2 deletions
@@ -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 &note);
void sendErrorToClientViaExecutor(int id, const std::string &error);
template <typename C>
void runInExecutor(int id, C callback) {
folly::via(executor_.get(), [cb = std::move(callback)]() { cb(); });
}
std::shared_ptr<RuntimeAdapter> runtimeAdapter_;
std::string title_;
@@ -721,6 +728,60 @@ void Connection::Impl::handle(
.thenError<std::exception>(sendErrorToClient(req.id));
}
void Connection::Impl::handle(const m::profiler::StartRequest &req) {
auto *hermesRT = dynamic_cast<HermesRuntime *>(&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<HermesRuntime *>(&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
@@ -1,5 +1,5 @@
// Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved.
// @generated SignedSource<<130ce3da2ad67004eb7bb91f19028a89>>
// @generated SignedSource<<f6f9a72f332587809b4e50ab054e0a74>>
#include "MessageTypes.h"
@@ -60,6 +60,8 @@ std::unique_ptr<Request> Request::fromJsonThrowOnError(const std::string &str) {
makeUnique<heapProfiler::StopTrackingHeapObjectsRequest>},
{"HeapProfiler.takeHeapSnapshot",
makeUnique<heapProfiler::TakeHeapSnapshotRequest>},
{"Profiler.start", makeUnique<profiler::StartRequest>},
{"Profiler.stop", makeUnique<profiler::StopRequest>},
{"Runtime.callFunctionOn", makeUnique<runtime::CallFunctionOnRequest>},
{"Runtime.evaluate", makeUnique<runtime::EvaluateRequest>},
{"Runtime.getHeapUsage", makeUnique<runtime::GetHeapUsageRequest>},
@@ -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");
@@ -1,5 +1,5 @@
// Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved.
// @generated SignedSource<<41786947d74eb3f47d8168b82b5ccf85>>
// @generated SignedSource<<fcbcfeecbc72ca30c8ed005ad47839bb>>
#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<heapProfiler::SamplingHeapProfileSample> 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<int> hitCount;
folly::Optional<std::vector<int>> children;
folly::Optional<std::string> deoptReason;
folly::Optional<std::vector<profiler::PositionTickInfo>> positionTicks;
};
struct profiler::Profile : public Serializable {
Profile() = default;
explicit Profile(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
std::vector<profiler::ProfileNode> nodes;
double startTime{};
double endTime{};
folly::Optional<std::vector<int>> samples;
folly::Optional<std::vector<int>> 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<bool> 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);
@@ -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
@@ -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<std::string> 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
@@ -2837,6 +2837,53 @@ TEST(ConnectionTests, heapSnapshotRemoteObject) {
expectNotification<m::debugger::ResumedNotification>(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<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// Start the sampling profiler. At this point it is not safe to manipulate the
// VM, so...
send<m::profiler::StartRequest>(conn, msgId++);
// Disable the debugger.
send<m::debugger::DisableRequest>(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<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
// Being re-attached to the VM, send the stop sampling profile request.
{
auto resp = send<m::profiler::StopRequest, m::profiler::StopResponse>(
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
@@ -28,6 +28,8 @@ HeapProfiler.heapStatsUpdate
HeapProfiler.lastSeenObjectId
HeapProfiler.getObjectByHeapObjectId
HeapProfiler.getHeapObjectId
Profiler.start
Profiler.stop
Runtime.callFunctionOn
Runtime.consoleAPICalled
Runtime.evaluate