Serialize Hermes Profile to Tracing Profile (#49082)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/49082

# Changelog: [Internal]

> NOTE: Some CI jobs are expected to fail, because changes in Hermes D67353585 should be landed first, and then grafted to Static Hermes.

In this diff we will:
- Call newly added API in Hermes from `HermesRuntimeTargetDelegate.cpp`
- Define format for local Sampling Profile that will be used in Tracing domain
- Implement formatter for Hermes Profile -> Tracign Profile

Reviewed By: bgirard

Differential Revision: D68414421

fbshipit-source-id: 05d76e9bcff46f88a2338490a9d858cb121f72cc
This commit is contained in:
Ruslan Lesiutin
2025-02-27 08:32:12 -08:00
committed by Facebook GitHub Bot
parent 067c5f9954
commit 1aced32a1c
4 changed files with 338 additions and 2 deletions
@@ -0,0 +1,168 @@
/*
* 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 "HermesRuntimeSamplingProfileSerializer.h"
namespace facebook::react::jsinspector_modern::tracing {
namespace {
/// Fallback script ID for call frames, when Hermes didn't provide one or when
/// this frame is part of the VM, like native functions, used for parity with
/// Chromium + V8.
const uint32_t FALLBACK_SCRIPT_ID = 0;
/// Garbage collector frame name, used for parity with Chromium + V8.
const std::string GARBAGE_COLLECTOR_FRAME_NAME = "(garbage collector)";
/// Filters out Hermes Suspend frames related to Debugger.
/// Even though Debugger domain is expected to be disabled, Hermes might run
/// Debugger loop while recording sampling profile. We only allow GC frames.
bool shouldIgnoreHermesFrame(
hermes::sampling_profiler::ProfileSampleCallStackFrame* hermesFrame) {
if (hermesFrame->getKind() !=
hermes::sampling_profiler::ProfileSampleCallStackFrame::Kind::Suspend) {
return false;
}
auto* suspendFrame = static_cast<
hermes::sampling_profiler::ProfileSampleCallStackSuspendFrame*>(
hermesFrame);
auto suspendFrameKind = suspendFrame->getSuspendFrameKind();
return suspendFrameKind !=
hermes::sampling_profiler::ProfileSampleCallStackSuspendFrame::
SuspendFrameKind::GC;
}
RuntimeSamplingProfile::SampleCallStackFrame convertHermesFrameToTracingFrame(
hermes::sampling_profiler::ProfileSampleCallStackFrame* hermesFrame) {
switch (hermesFrame->getKind()) {
case hermes::sampling_profiler::ProfileSampleCallStackFrame::Kind::
JSFunction: {
auto* jsFunctionFrame = static_cast<
hermes::sampling_profiler::ProfileSampleCallStackJSFunctionFrame*>(
hermesFrame);
return RuntimeSamplingProfile::SampleCallStackFrame{
RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction,
jsFunctionFrame->hasScriptId() ? jsFunctionFrame->getScriptId()
: FALLBACK_SCRIPT_ID,
jsFunctionFrame->getFunctionName(),
jsFunctionFrame->hasUrl()
? std::optional<std::string>{jsFunctionFrame->getUrl()}
: std::nullopt,
jsFunctionFrame->hasLineNumber()
? std::optional<uint32_t>{jsFunctionFrame->getLineNumber() - 1}
// Hermes VM keeps line numbers as 1-based. Convert to
// 0-based.
: std::nullopt,
jsFunctionFrame->hasColumnNumber()
? std::optional<uint32_t>{jsFunctionFrame->getColumnNumber() - 1}
// Hermes VM keeps column numbers as 1-based. Convert to
// 0-based.
: std::nullopt,
};
}
case hermes::sampling_profiler::ProfileSampleCallStackFrame::Kind::
NativeFunction: {
auto* nativeFunctionFrame =
static_cast<hermes::sampling_profiler::
ProfileSampleCallStackNativeFunctionFrame*>(
hermesFrame);
return RuntimeSamplingProfile::SampleCallStackFrame{
RuntimeSamplingProfile::SampleCallStackFrame::Kind::NativeFunction,
FALLBACK_SCRIPT_ID, // JavaScript Runtime defines the implementation
// for native function, no script ID to reference.
nativeFunctionFrame->getFunctionName(),
};
}
case hermes::sampling_profiler::ProfileSampleCallStackFrame::Kind::
HostFunction: {
auto* hostFunctionFrame = static_cast<
hermes::sampling_profiler::ProfileSampleCallStackHostFunctionFrame*>(
hermesFrame);
return RuntimeSamplingProfile::SampleCallStackFrame{
RuntimeSamplingProfile::SampleCallStackFrame::Kind::HostFunction,
FALLBACK_SCRIPT_ID, // JavaScript Runtime defines the implementation
// for host function, no script ID to reference.
hostFunctionFrame->getFunctionName(),
};
}
case hermes::sampling_profiler::ProfileSampleCallStackFrame::Kind::
Suspend: {
auto* suspendFrame = static_cast<
hermes::sampling_profiler::ProfileSampleCallStackSuspendFrame*>(
hermesFrame);
auto suspendFrameKind = suspendFrame->getSuspendFrameKind();
if (suspendFrameKind ==
hermes::sampling_profiler::ProfileSampleCallStackSuspendFrame::
SuspendFrameKind::GC) {
return RuntimeSamplingProfile::SampleCallStackFrame{
RuntimeSamplingProfile::SampleCallStackFrame::Kind::
GarbageCollector,
FALLBACK_SCRIPT_ID, // GC frames are part of the VM, no script ID to
// reference.
GARBAGE_COLLECTOR_FRAME_NAME,
};
}
// We should have filtered out Debugger Suspend frames before in
// shouldFilterOutHermesFrame().
throw std::logic_error{
"Unexpected Suspend frame found in Hermes call stack"};
}
default:
throw std::logic_error{"Unknown Hermes stack frame kind"};
}
}
RuntimeSamplingProfile::Sample convertHermesSampleToTracingSample(
hermes::sampling_profiler::ProfileSample& hermesSample) {
uint64_t reconciledTimestamp = hermesSample.getTimestamp();
std::vector<hermes::sampling_profiler::ProfileSampleCallStackFrame*>
hermesSampleCallStack = hermesSample.getCallStack();
std::vector<RuntimeSamplingProfile::SampleCallStackFrame>
reconciledSampleCallStack;
reconciledSampleCallStack.reserve(hermesSampleCallStack.size());
for (auto* hermesFrame : hermesSampleCallStack) {
if (shouldIgnoreHermesFrame(hermesFrame)) {
continue;
}
RuntimeSamplingProfile::SampleCallStackFrame reconciledFrame =
convertHermesFrameToTracingFrame(hermesFrame);
reconciledSampleCallStack.push_back(std::move(reconciledFrame));
}
return RuntimeSamplingProfile::Sample{
reconciledTimestamp,
hermesSample.getThreadId(),
std::move(reconciledSampleCallStack)};
}
} // namespace
/* static */ RuntimeSamplingProfile
HermesRuntimeSamplingProfileSerializer::serializeToTracingSamplingProfile(
const hermes::sampling_profiler::Profile& hermesProfile) {
std::vector<hermes::sampling_profiler::ProfileSample> hermesSamples =
hermesProfile.getSamples();
std::vector<RuntimeSamplingProfile::Sample> reconciledSamples;
reconciledSamples.reserve(hermesSamples.size());
for (auto& hermesSample : hermesSamples) {
RuntimeSamplingProfile::Sample reconciledSample =
convertHermesSampleToTracingSample(hermesSample);
reconciledSamples.push_back(std::move(reconciledSample));
}
return RuntimeSamplingProfile{"Hermes", std::move(reconciledSamples)};
}
} // namespace facebook::react::jsinspector_modern::tracing
@@ -0,0 +1,22 @@
/*
* 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.
*/
#pragma once
#include <hermes/hermes.h>
#include <jsinspector-modern/tracing/RuntimeSamplingProfile.h>
namespace facebook::react::jsinspector_modern::tracing {
class HermesRuntimeSamplingProfileSerializer {
public:
static tracing::RuntimeSamplingProfile serializeToTracingSamplingProfile(
const hermes::sampling_profiler::Profile& hermesProfile);
};
} // namespace facebook::react::jsinspector_modern::tracing
@@ -7,6 +7,7 @@
#include <jsinspector-modern/RuntimeTarget.h>
#include "HermesRuntimeSamplingProfileSerializer.h"
#include "HermesRuntimeTargetDelegate.h"
// If HERMES_ENABLE_DEBUGGER isn't defined, we can't access any Hermes
@@ -182,7 +183,9 @@ class HermesRuntimeTargetDelegate::Impl final : public RuntimeTargetDelegate {
}
tracing::RuntimeSamplingProfile collectSamplingProfile() override {
return tracing::RuntimeSamplingProfile{};
return tracing::HermesRuntimeSamplingProfileSerializer::
serializeToTracingSamplingProfile(
runtime_->dumpSampledTraceToProfile());
}
private:
@@ -7,8 +7,151 @@
#pragma once
#include <optional>
#include <string>
#include <utility>
#include <vector>
namespace facebook::react::jsinspector_modern::tracing {
struct RuntimeSamplingProfile {};
/// Contains relevant information about the sampled runtime from start to
/// finish.
struct RuntimeSamplingProfile {
public:
/// Represents a single frame inside the captured sample stack.
struct SampleCallStackFrame {
/// Represents type of frame inside of recorded call stack.
enum class Kind {
JSFunction, /// JavaScript function frame.
NativeFunction, /// Native built-in functions, like arrayPrototypeMap.
HostFunction, /// Native functions, defined by Host, a.k.a. Host
/// functions.
GarbageCollector, /// Garbage collection frame.
};
public:
SampleCallStackFrame(
const Kind kind,
const uint32_t scriptId,
std::string functionName,
std::optional<std::string> url = std::nullopt,
const std::optional<uint32_t>& lineNumber = std::nullopt,
const std::optional<uint32_t>& columnNumber = std::nullopt)
: kind_(kind),
scriptId_(scriptId),
functionName_(std::move(functionName)),
url_(std::move(url)),
lineNumber_(lineNumber),
columnNumber_(columnNumber) {}
/// \return type of the call stack frame.
Kind getKind() const {
return kind_;
}
/// \return id of the corresponding script in the VM.
uint32_t getScriptId() const {
return scriptId_;
}
/// \return name of the function that represents call frame.
const std::string& getFunctionName() const {
return functionName_;
}
bool hasUrl() const {
return url_.has_value();
}
/// \return source url of the corresponding script in the VM.
const std::string& getUrl() const {
return url_.value();
}
bool hasLineNumber() const {
return lineNumber_.has_value();
}
/// \return 0-based line number of the corresponding call frame.
uint32_t getLineNumber() const {
return lineNumber_.value();
}
bool hasColumnNumber() const {
return columnNumber_.has_value();
}
/// \return 0-based column number of the corresponding call frame.
uint32_t getColumnNumber() const {
return columnNumber_.value();
}
private:
Kind kind_;
uint32_t scriptId_;
std::string functionName_;
std::optional<std::string> url_;
std::optional<uint32_t> lineNumber_;
std::optional<uint32_t> columnNumber_;
};
/// A pair of a timestamp and a snapshot of the call stack at this point in
/// time.
struct Sample {
public:
Sample(
uint64_t timestamp,
uint64_t threadId,
std::vector<SampleCallStackFrame> callStack)
: timestamp_(timestamp),
threadId_(threadId),
callStack_(std::move(callStack)) {}
/// \return serialized unix timestamp in microseconds granularity. The
/// moment when this sample was recorded.
uint64_t getTimestamp() const {
return timestamp_;
}
/// \return thread id where sample was recorded.
uint64_t getThreadId() const {
return threadId_;
}
/// \return a snapshot of the call stack. The first element of the vector is
/// the lowest frame in the stack.
const std::vector<SampleCallStackFrame>& getCallStack() const {
return callStack_;
}
private:
/// When the call stack snapshot was taken (μs).
uint64_t timestamp_;
/// Thread id where sample was recorded.
uint64_t threadId_;
/// Snapshot of the call stack. The first element of the vector is
/// the lowest frame in the stack.
std::vector<SampleCallStackFrame> callStack_;
};
RuntimeSamplingProfile(std::string runtimeName, std::vector<Sample> samples)
: runtimeName_(std::move(runtimeName)), samples_(std::move(samples)) {}
/// \return name of the JavaScript runtime, where sampling occurred.
const std::string& getRuntimeName() const {
return runtimeName_;
}
/// \return list of recorded samples, should be chronologically sorted.
const std::vector<Sample>& getSamples() const {
return samples_;
}
private:
/// Name of the runtime, where sampling occurred: Hermes, V8, etc.
std::string runtimeName_;
/// List of recorded samples, should be chronologically sorted.
std::vector<Sample> samples_;
};
} // namespace facebook::react::jsinspector_modern::tracing