From 1aced32a1cd82dfeee1a5b1f164b4ff64ac24ce8 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Thu, 27 Feb 2025 08:32:12 -0800 Subject: [PATCH] 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 --- ...HermesRuntimeSamplingProfileSerializer.cpp | 168 ++++++++++++++++++ .../HermesRuntimeSamplingProfileSerializer.h | 22 +++ .../chrome/HermesRuntimeTargetDelegate.cpp | 5 +- .../tracing/RuntimeSamplingProfile.h | 145 ++++++++++++++- 4 files changed, 338 insertions(+), 2 deletions(-) create mode 100644 packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeSamplingProfileSerializer.cpp create mode 100644 packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeSamplingProfileSerializer.h diff --git a/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeSamplingProfileSerializer.cpp b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeSamplingProfileSerializer.cpp new file mode 100644 index 00000000000..20eabc56dd6 --- /dev/null +++ b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeSamplingProfileSerializer.cpp @@ -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{jsFunctionFrame->getUrl()} + : std::nullopt, + jsFunctionFrame->hasLineNumber() + ? std::optional{jsFunctionFrame->getLineNumber() - 1} + // Hermes VM keeps line numbers as 1-based. Convert to + // 0-based. + : std::nullopt, + jsFunctionFrame->hasColumnNumber() + ? std::optional{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( + 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 + hermesSampleCallStack = hermesSample.getCallStack(); + + std::vector + 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 hermesSamples = + hermesProfile.getSamples(); + std::vector 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 diff --git a/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeSamplingProfileSerializer.h b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeSamplingProfileSerializer.h new file mode 100644 index 00000000000..1e205af8524 --- /dev/null +++ b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeSamplingProfileSerializer.h @@ -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 + +#include + +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 diff --git a/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.cpp b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.cpp index 4d2503cf703..30c88793ae9 100644 --- a/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.cpp +++ b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.cpp @@ -7,6 +7,7 @@ #include +#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: diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfile.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfile.h index ff3d2effdc2..f51d3523c0c 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfile.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfile.h @@ -7,8 +7,151 @@ #pragma once +#include +#include +#include +#include + 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 url = std::nullopt, + const std::optional& lineNumber = std::nullopt, + const std::optional& 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 url_; + std::optional lineNumber_; + std::optional 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 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& 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 callStack_; + }; + + RuntimeSamplingProfile(std::string runtimeName, std::vector 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& 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 samples_; +}; } // namespace facebook::react::jsinspector_modern::tracing