Skip to content

Commit

Permalink
Introduce format-agnostic API for JS Sampling (#1603)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #1603

This diff adds a new API on `HermesRuntime`, which will emit a `struct` with all necessary information about completed JavaScript Sampling Profile.

This `struct` can later be used by other third parties, such as React Native. The reason for creating a data source, and not just providing a stream to which Hermes will emit serialized information is that on React Native side we own the format in which data should be serialized, together with the data from other potential sources, such as User Timings, Interactions, Network, etc.

HermesRuntime will have 2 new API endpoints:
- `dumpAsProfile`. A method on `HermesRuntime` instance, which returns `Profile` that contains all relevant information about the recorded sampled stack trace.
- `dumpAsProfilesGlobal`. A static method, which returns vector of all recorded Profiles for all registered sampling profiler instances.

The actual conversion to Trace Event Format will hapen on React Native side, Hermes will only emit data structure that is agnostic to format and operates only with JavaScript runtime-level entities and general stuff: call stack frame information, timestamps, information about OS process and thread where sampling occured.

Reviewed By: dannysu

Differential Revision: D67353585

fbshipit-source-id: 291a3daee4114e4fe3545bbcb94faa8ecc534443
  • Loading branch information
hoxyq authored and facebook-github-bot committed Feb 4, 2025
1 parent a998e2f commit 37437be
Show file tree
Hide file tree
Showing 8 changed files with 551 additions and 0 deletions.
21 changes: 21 additions & 0 deletions API/hermes/hermes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,27 @@ void HermesRuntime::sampledTraceToStreamInDevToolsFormat(std::ostream &stream) {
#endif // HERMESVM_SAMPLING_PROFILER_AVAILABLE
}

sampling_profiler::Profile HermesRuntime::dumpSampledTraceToProfile() {
#if HERMESVM_SAMPLING_PROFILER_AVAILABLE
vm::SamplingProfiler *sp = impl(this)->runtime_.samplingProfiler.get();
if (!sp) {
throw jsi::JSINativeException("Runtime not registered for profiling");
}
return sp->dumpAsProfile();
#else
throwHermesNotCompiledWithSamplingProfilerSupport();
#endif // HERMESVM_SAMPLING_PROFILER_AVAILABLE
}

std::vector<sampling_profiler::Profile>
HermesRuntime::dumpSampledTraceToProfilesGlobal() {
#if HERMESVM_SAMPLING_PROFILER_AVAILABLE
return ::hermes::vm::SamplingProfiler::dumpAsProfilesGlobal();
#else
throwHermesNotCompiledWithSamplingProfilerSupport();
#endif // HERMESVM_SAMPLING_PROFILER_AVAILABLE
}

/*static*/ std::unordered_map<std::string, std::vector<std::string>>
HermesRuntime::getExecutedFunctions() {
std::unordered_map<
Expand Down
10 changes: 10 additions & 0 deletions API/hermes/hermes.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

#include <hermes/Public/HermesExport.h>
#include <hermes/Public/RuntimeConfig.h>
#include <hermes/Public/SamplingProfiler.h>
#include <jsi/jsi.h>
#include <unordered_map>

Expand Down Expand Up @@ -89,6 +90,15 @@ class HERMES_EXPORT HermesRuntime : public jsi::Runtime {
/// Profiler.stop return type.
void sampledTraceToStreamInDevToolsFormat(std::ostream &stream);

/// Dump sampled stack trace for a given runtime to a data structure that can
/// be used by third parties.
sampling_profiler::Profile dumpSampledTraceToProfile();

/// Dump sampled stack trace for all registered local sampling profiler
/// instances to a data structure that can be used by third parties.
static std::vector<sampling_profiler::Profile>
dumpSampledTraceToProfilesGlobal();

/// Return the executed JavaScript function info.
/// This information holds the segmentID, Virtualoffset and sourceURL.
/// This information is needed specifically to be able to symbolicate non-CJS
Expand Down
10 changes: 10 additions & 0 deletions include/hermes/VM/Profiler/SamplingProfiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#if HERMESVM_SAMPLING_PROFILER_AVAILABLE

#include "hermes/Public/SamplingProfiler.h"
#include "hermes/VM/Callable.h"
#include "hermes/VM/JSNativeFunctions.h"
#include "hermes/VM/Runtime.h"
Expand Down Expand Up @@ -267,6 +268,10 @@ class SamplingProfiler {
/// Dump sampled stack to \p OS in chrome trace format.
void dumpChromeTrace(llvh::raw_ostream &OS);

/// Dump sampled stack trace to data structure that can be used by third
/// parties.
facebook::hermes::sampling_profiler::Profile dumpAsProfile();

/// Dump the sampled stack to \p OS in the format expected by the
/// Profiler.stop return type. See
///
Expand All @@ -281,6 +286,11 @@ class SamplingProfiler {
/// Static wrapper for dumpChromeTrace.
static void dumpChromeTraceGlobal(llvh::raw_ostream &OS);

/// Static wrapper for dumpAsProfile. Will dump in separate Profile for each
/// local sampling profiler instance.
static std::vector<facebook::hermes::sampling_profiler::Profile>
dumpAsProfilesGlobal();

/// Enable and start profiling.
static bool enable(double meanHzFreq = 100);

Expand Down
1 change: 1 addition & 0 deletions lib/VM/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ set(source_files
Profiler/ChromeTraceSerializer.cpp
Profiler/CodeCoverageProfiler.cpp
Profiler/InlineCacheProfiler.cpp
Profiler/ProfileGenerator.cpp
Profiler/SamplingProfiler.cpp
Profiler/SamplingProfilerPosix.cpp
Profiler/SamplingProfilerWindows.cpp
Expand Down
166 changes: 166 additions & 0 deletions lib/VM/Profiler/ProfileGenerator.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* 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 "ProfileGenerator.h"

#if HERMESVM_SAMPLING_PROFILER_AVAILABLE

namespace fhsp = ::facebook::hermes::sampling_profiler;

namespace hermes {
namespace vm {

namespace {

/// \return timestamp as time since epoch in microseconds.
static uint64_t convertTimestampToMicroseconds(
SamplingProfiler::TimeStampType timeStamp) {
return std::chrono::duration_cast<std::chrono::microseconds>(
timeStamp.time_since_epoch())
.count();
}

static std::string getJSFunctionName(
hbc::BCProvider *bcProvider,
uint32_t funcId) {
hbc::RuntimeFunctionHeader functionHeader =
bcProvider->getFunctionHeader(funcId);
return bcProvider->getStringRefFromID(functionHeader.functionName()).str();
}

static OptValue<hbc::DebugSourceLocation> getSourceLocation(
hbc::BCProvider *bcProvider,
uint32_t funcId,
uint32_t opcodeOffset) {
const hbc::DebugOffsets *debugOffsets = bcProvider->getDebugOffsets(funcId);
if (debugOffsets &&
debugOffsets->sourceLocations != hbc::DebugOffsets::NO_OFFSET) {
return bcProvider->getDebugInfo()->getLocationForAddress(
debugOffsets->sourceLocations, opcodeOffset);
}
return llvh::None;
}

static fhsp::ProfileSampleCallStackSuspendFrame::SuspendFrameKind
formatSuspendFrameKind(SamplingProfiler::SuspendFrameInfo::Kind kind) {
switch (kind) {
case SamplingProfiler::SuspendFrameInfo::Kind::GC:
return fhsp::ProfileSampleCallStackSuspendFrame::SuspendFrameKind::GC;
case SamplingProfiler::SuspendFrameInfo::Kind::Debugger:
return fhsp::ProfileSampleCallStackSuspendFrame::SuspendFrameKind::
Debugger;
case SamplingProfiler::SuspendFrameInfo::Kind::Multiple:
return fhsp::ProfileSampleCallStackSuspendFrame::SuspendFrameKind::
Multiple;

default:
llvm_unreachable("Unexpected Suspend Frame kind");
}
}

/// Format VM-level frame to public interface.
static fhsp::ProfileSampleCallStackFrame *formatCallStackFrame(
const SamplingProfiler::StackFrame &frame,
const SamplingProfiler &samplingProfiler) {
switch (frame.kind) {
case SamplingProfiler::StackFrame::FrameKind::SuspendFrame: {
return new fhsp::ProfileSampleCallStackSuspendFrame{
formatSuspendFrameKind(frame.suspendFrame.kind),
};
}

case SamplingProfiler::StackFrame::FrameKind::NativeFunction:
return new fhsp::ProfileSampleCallStackNativeFunctionFrame{
samplingProfiler.getNativeFunctionName(frame),
};

case SamplingProfiler::StackFrame::FrameKind::FinalizableNativeFunction:
return new fhsp::ProfileSampleCallStackHostFunctionFrame{
samplingProfiler.getNativeFunctionName(frame),
};

case SamplingProfiler::StackFrame::FrameKind::JSFunction: {
RuntimeModule *module = frame.jsFrame.module;
hbc::BCProvider *bcProvider = module->getBytecode();

std::string functionName =
getJSFunctionName(bcProvider, frame.jsFrame.functionId);
std::optional<uint32_t> scriptId = std::nullopt;
std::optional<std::string> url = std::nullopt;
std::optional<uint32_t> lineNumber = std::nullopt;
std::optional<uint32_t> columnNumber = std::nullopt;

OptValue<hbc::DebugSourceLocation> sourceLocOpt = getSourceLocation(
bcProvider, frame.jsFrame.functionId, frame.jsFrame.offset);
if (sourceLocOpt.hasValue()) {
// Bundle has debug info.
scriptId = sourceLocOpt.getValue().filenameId;
url = bcProvider->getDebugInfo()->getFilenameByID(scriptId.value());

// hbc::DebugSourceLocation is 1-based, but initializes line and column
// fields with 0 by default.
uint32_t line = sourceLocOpt.getValue().line;
uint32_t column = sourceLocOpt.getValue().column;
if (line != 0) {
lineNumber = line;
}
if (column != 0) {
columnNumber = column;
}
}

return new fhsp::ProfileSampleCallStackJSFunctionFrame{
functionName,
scriptId,
url,
lineNumber,
columnNumber,
};
}

default:
llvm_unreachable("Unexpected Frame kind");
}
}

} // namespace

/* static */ fhsp::Profile ProfileGenerator::generate(
const SamplingProfiler &sp,
uint32_t pid,
const SamplingProfiler::ThreadNamesMap &threadNames,
const std::vector<SamplingProfiler::StackTrace> &sampledStacks) {
fhsp::Process process{pid};

assert(!threadNames.empty() && "Expected at least one Thread recorded");
// It is unclear why Hermes keeps map of threads for a local profiler.
// See D67453370 for more context, this map is expected to contain a single
// entry.
auto threadEntry = threadNames.begin();
fhsp::Thread thread{threadEntry->first, threadEntry->second};

std::vector<fhsp::ProfileSample> samples;
samples.reserve(sampledStacks.size());
for (const SamplingProfiler::StackTrace &sampledStack : sampledStacks) {
uint64_t timestamp = convertTimestampToMicroseconds(sampledStack.timeStamp);

std::vector<fhsp::ProfileSampleCallStackFrame *> callFrames;
callFrames.reserve(sampledStack.stack.size());
for (const SamplingProfiler::StackFrame &frame : sampledStack.stack) {
callFrames.emplace_back(formatCallStackFrame(frame, sp));
}

samples.emplace_back(timestamp, callFrames);
}

return fhsp::Profile{process, thread, samples};
}

} // namespace vm
} // namespace hermes

#endif // HERMESVM_SAMPLING_PROFILER_AVAILABLE
40 changes: 40 additions & 0 deletions lib/VM/Profiler/ProfileGenerator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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.
*/

#ifndef HERMES_VM_PROFILER_PROFILEGENERATOR_H
#define HERMES_VM_PROFILER_PROFILEGENERATOR_H

#include "hermes/VM/Profiler/SamplingProfilerDefs.h"

#if HERMESVM_SAMPLING_PROFILER_AVAILABLE

#include "hermes/VM/Profiler/SamplingProfiler.h"

namespace hermes {
namespace vm {

/// Generate format-agnostic data structure, which should contain relevant
/// information about the recorded Sampling Profile and may be used by third
/// parties.
class ProfileGenerator {
ProfileGenerator() = delete;

public:
/// Emit Profile in a single struct.
static facebook::hermes::sampling_profiler::Profile generate(
const SamplingProfiler &sp,
uint32_t pid,
const SamplingProfiler::ThreadNamesMap &threadNames,
const std::vector<SamplingProfiler::StackTrace> &sampledStacks);
};

} // namespace vm
} // namespace hermes

#endif // HERMESVM_SAMPLING_PROFILER_AVAILABLE

#endif // HERMES_VM_PROFILER_PROFILEGENERATOR_H
26 changes: 26 additions & 0 deletions lib/VM/Profiler/SamplingProfiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "llvh/Support/Compiler.h"

#include "ChromeTraceSerializer.h"
#include "ProfileGenerator.h"
#include "SamplingProfilerSampler.h"

#include <fcntl.h>
Expand Down Expand Up @@ -207,6 +208,31 @@ void SamplingProfiler::serializeInDevToolsFormat(llvh::raw_ostream &OS) {
clear();
}

std::vector<facebook::hermes::sampling_profiler::Profile>
SamplingProfiler::dumpAsProfilesGlobal() {
auto globalProfiler = sampling_profiler::Sampler::get();
std::lock_guard<std::mutex> lk(globalProfiler->profilerLock_);

std::vector<facebook::hermes::sampling_profiler::Profile> profiles;
for (auto *currentProfilerInstance : globalProfiler->profilers_) {
auto profileForCurrentInstance = currentProfilerInstance->dumpAsProfile();
profiles.push_back(std::move(profileForCurrentInstance));
}

return profiles;
}

facebook::hermes::sampling_profiler::Profile SamplingProfiler::dumpAsProfile() {
std::lock_guard<std::mutex> lk(runtimeDataLock_);
auto pid = oscompat::process_id();

facebook::hermes::sampling_profiler::Profile profile =
ProfileGenerator::generate(*this, pid, threadNames_, sampledStacks_);

clear();
return profile;
}

bool SamplingProfiler::enable(double meanHzFreq) {
return sampling_profiler::Sampler::get()->enable(meanHzFreq);
}
Expand Down
Loading

0 comments on commit 37437be

Please sign in to comment.