Implement sampling profile serializer (#49191)

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

# Changelog: [Internal]

In this diff we are adding another serializer, that will receive local sampling profile (in tracing domain), and will record corresponding Trace Events with `PerformanceTracer`.

It encapsulates the logic of transforming list of samples to `"Profile"` and `"ProfileChunk"` trace events, which will be parsed by Chrome DevTools later.

Reviewed By: huntie

Differential Revision: D68439735

fbshipit-source-id: 0b3f2b3aff5b79a921e0350759e93f5b05e34d8e
This commit is contained in:
Ruslan Lesiutin
2025-02-27 08:32:12 -08:00
committed by Facebook GitHub Bot
parent 290f237cfa
commit 87d4300f14
5 changed files with 436 additions and 0 deletions
@@ -0,0 +1,111 @@
/*
* 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 "RuntimeSamplingProfile.h"
namespace facebook::react::jsinspector_modern::tracing {
/*
* Auxiliary data structure used for creating Profile tree and identifying
* identical frames.
*/
class ProfileTreeNode {
public:
/*
* For Chromium & V8 this could also be WASM, this is not the case for us.
*/
enum class CodeType {
JavaScript,
Other,
};
ProfileTreeNode(
uint32_t id,
CodeType codeType,
ProfileTreeNode* parent,
RuntimeSamplingProfile::SampleCallStackFrame callFrame)
: id_(id),
codeType_(codeType),
parent_(parent),
callFrame_(std::move(callFrame)) {}
uint32_t getId() const {
return id_;
}
CodeType getCodeType() const {
return codeType_;
}
/**
* \return pointer to the parent node, nullptr if this is the root node.
*/
ProfileTreeNode* getParent() const {
return parent_;
}
/**
* \return call frame information that is represented by this node.
*/
const RuntimeSamplingProfile::SampleCallStackFrame& getCallFrame() const {
return callFrame_;
}
/**
* Will only add unique child node. Returns pointer to the already existing
* child node, nullptr if the added child node is unique.
*/
ProfileTreeNode* addChild(ProfileTreeNode* child) {
for (auto existingChild : children_) {
if (*existingChild == child) {
return existingChild;
}
}
children_.push_back(child);
return nullptr;
}
bool operator==(const ProfileTreeNode* rhs) const {
if (this->parent_ != rhs->parent_) {
return false;
}
if (this->codeType_ != rhs->codeType_) {
return false;
}
return this->getCallFrame() == rhs->getCallFrame();
}
private:
/**
* Unique id of the node.
*/
uint32_t id_;
/**
* Type of the code that is represented by this node. Either JavaScript or
* Other.
*/
CodeType codeType_;
/**
* Pointer to the parent node. Should be nullptr only for root node.
*/
ProfileTreeNode* parent_{nullptr};
/**
* Lst of pointers to children nodes.
*/
std::vector<ProfileTreeNode*> children_{};
/**
* Information about the corresponding call frame that is represented by this
* node.
*/
RuntimeSamplingProfile::SampleCallStackFrame callFrame_;
};
} // namespace facebook::react::jsinspector_modern::tracing
@@ -86,6 +86,12 @@ struct RuntimeSamplingProfile {
return columnNumber_.value();
}
inline bool operator==(const SampleCallStackFrame& rhs) const noexcept {
return kind_ == rhs.kind_ && scriptId_ == rhs.scriptId_ &&
functionName_ == rhs.functionName_ && url_ == rhs.url_ &&
lineNumber_ == rhs.lineNumber_ && columnNumber_ == rhs.columnNumber_;
}
private:
Kind kind_;
uint32_t scriptId_;
@@ -0,0 +1,231 @@
/*
* 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 "RuntimeSamplingProfileTraceEventSerializer.h"
#include "ProfileTreeNode.h"
namespace facebook::react::jsinspector_modern::tracing {
namespace {
uint64_t formatTimePointToUnixTimestamp(
std::chrono::steady_clock::time_point timestamp) {
return std::chrono::duration_cast<std::chrono::microseconds>(
timestamp.time_since_epoch())
.count();
}
TraceEventProfileChunk::CPUProfile::Node convertToTraceEventProfileNode(
ProfileTreeNode* node) {
ProfileTreeNode* nodeParent = node->getParent();
const RuntimeSamplingProfile::SampleCallStackFrame& callFrame =
node->getCallFrame();
auto traceEventCallFrame =
TraceEventProfileChunk::CPUProfile::Node::CallFrame{
node->getCodeType() == ProfileTreeNode::CodeType::JavaScript
? "JS"
: "other",
callFrame.getScriptId(),
callFrame.getFunctionName(),
callFrame.hasUrl() ? std::optional<std::string>(callFrame.getUrl())
: std::nullopt,
callFrame.hasLineNumber()
? std::optional<uint32_t>(callFrame.getLineNumber())
: std::nullopt,
callFrame.hasColumnNumber()
? std::optional<uint32_t>(callFrame.getColumnNumber())
: std::nullopt};
return TraceEventProfileChunk::CPUProfile::Node{
node->getId(),
traceEventCallFrame,
nodeParent != nullptr ? std::optional<uint32_t>(nodeParent->getId())
: std::nullopt};
}
void emitSingleProfileChunk(
PerformanceTracer& performanceTracer,
uint16_t profileId,
uint64_t threadId,
uint64_t chunkTimestamp,
std::vector<ProfileTreeNode*>& nodes,
std::vector<uint32_t>& samples,
std::vector<long long>& timeDeltas) {
std::vector<TraceEventProfileChunk::CPUProfile::Node> traceEventNodes;
traceEventNodes.reserve(nodes.size());
for (ProfileTreeNode* node : nodes) {
traceEventNodes.push_back(convertToTraceEventProfileNode(node));
}
performanceTracer.reportRuntimeProfileChunk(
profileId,
threadId,
chunkTimestamp,
TraceEventProfileChunk{
TraceEventProfileChunk::CPUProfile{traceEventNodes, samples},
TraceEventProfileChunk::TimeDeltas{timeDeltas},
});
}
} // namespace
/* static */ void
RuntimeSamplingProfileTraceEventSerializer::serializeAndBuffer(
PerformanceTracer& performanceTracer,
const RuntimeSamplingProfile& profile,
std::chrono::steady_clock::time_point tracingStartTime,
uint16_t profileChunkSize) {
std::vector<RuntimeSamplingProfile::Sample> runtimeSamples =
profile.getSamples();
if (runtimeSamples.empty()) {
return;
}
uint64_t chunkThreadId = runtimeSamples.front().getThreadId();
uint64_t tracingStartUnixTimestamp =
formatTimePointToUnixTimestamp(tracingStartTime);
uint16_t profileId = performanceTracer.reportRuntimeProfile(
chunkThreadId, tracingStartUnixTimestamp);
uint32_t nodeCount = 0;
auto* rootNode = new ProfileTreeNode(
++nodeCount,
ProfileTreeNode::CodeType::Other,
nullptr,
RuntimeSamplingProfile::SampleCallStackFrame{
RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction,
0,
"(root)"});
auto* programNode = new ProfileTreeNode(
++nodeCount,
ProfileTreeNode::CodeType::Other,
rootNode,
RuntimeSamplingProfile::SampleCallStackFrame{
RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction,
0,
"(program)"});
auto* idleNode = new ProfileTreeNode(
++nodeCount,
ProfileTreeNode::CodeType::Other,
rootNode,
RuntimeSamplingProfile::SampleCallStackFrame{
RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction,
0,
"(idle)"});
rootNode->addChild(programNode);
rootNode->addChild(idleNode);
// Ideally, we should use a timestamp from Runtime Sampling Profiler.
// We currently use tracingStartTime, which is defined in TracingAgent.
uint64_t previousSampleTimestamp = tracingStartUnixTimestamp;
// There could be any number of new nodes in this chunk. Empty if all nodes
// are already emitted in previous chunks.
std::vector<ProfileTreeNode*> nodesInThisChunk;
nodesInThisChunk.push_back(rootNode);
nodesInThisChunk.push_back(programNode);
nodesInThisChunk.push_back(idleNode);
std::vector<uint32_t> samplesInThisChunk;
samplesInThisChunk.reserve(profileChunkSize);
std::vector<long long> timeDeltasInThisChunk;
timeDeltasInThisChunk.reserve(profileChunkSize);
RuntimeSamplingProfile::SampleCallStackFrame garbageCollectorCallFrame =
RuntimeSamplingProfile::SampleCallStackFrame{
RuntimeSamplingProfile::SampleCallStackFrame::Kind::GarbageCollector,
0,
"(garbage collector)"};
uint64_t chunkTimestamp = tracingStartUnixTimestamp;
for (const RuntimeSamplingProfile::Sample& sample : runtimeSamples) {
uint64_t sampleThreadId = sample.getThreadId();
// If next sample was recorded on a different thread, emit the current chunk
// and continue.
if (chunkThreadId != sampleThreadId) {
emitSingleProfileChunk(
performanceTracer,
profileId,
chunkThreadId,
chunkTimestamp,
nodesInThisChunk,
samplesInThisChunk,
timeDeltasInThisChunk);
nodesInThisChunk.clear();
samplesInThisChunk.clear();
timeDeltasInThisChunk.clear();
}
chunkThreadId = sampleThreadId;
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack =
sample.getCallStack();
uint64_t sampleTimestamp = sample.getTimestamp();
if (samplesInThisChunk.empty()) {
// New chunk. Reset the timestamp.
chunkTimestamp = sampleTimestamp;
}
long long timeDelta = sampleTimestamp - previousSampleTimestamp;
timeDeltasInThisChunk.push_back(timeDelta);
previousSampleTimestamp = sampleTimestamp;
ProfileTreeNode* previousNode = callStack.empty() ? idleNode : rootNode;
for (auto it = callStack.rbegin(); it != callStack.rend(); ++it) {
auto callFrame = *it;
bool isGarbageCollectorFrame = callFrame.getKind() ==
RuntimeSamplingProfile::SampleCallStackFrame::Kind::GarbageCollector;
// We don't need real garbage collector call frame, we change it to
// what Chrome DevTools expects.
auto* currentNode = new ProfileTreeNode(
nodeCount + 1,
isGarbageCollectorFrame ? ProfileTreeNode::CodeType::Other
: ProfileTreeNode::CodeType::JavaScript,
previousNode,
isGarbageCollectorFrame ? garbageCollectorCallFrame : callFrame);
ProfileTreeNode* alreadyExistingNode =
previousNode->addChild(currentNode);
if (alreadyExistingNode != nullptr) {
previousNode = alreadyExistingNode;
} else {
nodesInThisChunk.push_back(currentNode);
++nodeCount;
previousNode = currentNode;
}
}
samplesInThisChunk.push_back(previousNode->getId());
if (samplesInThisChunk.size() == profileChunkSize) {
emitSingleProfileChunk(
performanceTracer,
profileId,
chunkThreadId,
chunkTimestamp,
nodesInThisChunk,
samplesInThisChunk,
timeDeltasInThisChunk);
nodesInThisChunk.clear();
samplesInThisChunk.clear();
timeDeltasInThisChunk.clear();
}
}
if (!samplesInThisChunk.empty()) {
emitSingleProfileChunk(
performanceTracer,
profileId,
chunkThreadId,
chunkTimestamp,
nodesInThisChunk,
samplesInThisChunk,
timeDeltasInThisChunk);
}
}
} // namespace facebook::react::jsinspector_modern::tracing
@@ -0,0 +1,30 @@
/*
* 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 "PerformanceTracer.h"
#include "RuntimeSamplingProfile.h"
namespace facebook::react::jsinspector_modern::tracing {
/**
* Serializes RuntimeSamplingProfile into collection of specific Trace Events,
* which represent Profile information on a timeline.
*/
class RuntimeSamplingProfileTraceEventSerializer {
public:
RuntimeSamplingProfileTraceEventSerializer() = delete;
static void serializeAndBuffer(
PerformanceTracer& performanceTracer,
const RuntimeSamplingProfile& profile,
std::chrono::steady_clock::time_point tracingStartTime,
uint16_t profileChunkSize = 100);
};
} // namespace facebook::react::jsinspector_modern::tracing
@@ -0,0 +1,58 @@
/*
* 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 "ProfileTreeNode.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
namespace facebook::react::jsinspector_modern::tracing {
TEST(ProfileTreeNodeTest, EqualityOperator) {
auto callFrame = RuntimeSamplingProfile::SampleCallStackFrame{
RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction, 0, "foo"};
ProfileTreeNode* node1;
ProfileTreeNode* node2;
node1 = new ProfileTreeNode(
1, ProfileTreeNode::CodeType::JavaScript, nullptr, callFrame);
node2 = new ProfileTreeNode(
2, ProfileTreeNode::CodeType::JavaScript, nullptr, callFrame);
EXPECT_EQ(*node1 == node2, true);
node1 = new ProfileTreeNode(
3, ProfileTreeNode::CodeType::JavaScript, node1, callFrame);
node2 = new ProfileTreeNode(
4, ProfileTreeNode::CodeType::JavaScript, nullptr, callFrame);
EXPECT_EQ(*node1 == node2, false);
node1 = new ProfileTreeNode(
5, ProfileTreeNode::CodeType::JavaScript, node2, callFrame);
node2 = new ProfileTreeNode(
6, ProfileTreeNode::CodeType::JavaScript, node2, callFrame);
EXPECT_EQ(*node1 == node2, true);
}
TEST(ProfileTreeNodeTest, OnlyAddsUniqueChildren) {
auto callFrame = RuntimeSamplingProfile::SampleCallStackFrame{
RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction, 0, "foo"};
ProfileTreeNode* parent = new ProfileTreeNode(
1, ProfileTreeNode::CodeType::JavaScript, nullptr, callFrame);
ProfileTreeNode* existingChild = new ProfileTreeNode(
2, ProfileTreeNode::CodeType::JavaScript, parent, callFrame);
auto maybeAlreadyExistingChild = parent->addChild(existingChild);
EXPECT_EQ(maybeAlreadyExistingChild, nullptr);
auto copyOfExistingChild = new ProfileTreeNode(
3, ProfileTreeNode::CodeType::JavaScript, parent, callFrame);
maybeAlreadyExistingChild = parent->addChild(copyOfExistingChild);
EXPECT_EQ(maybeAlreadyExistingChild, existingChild);
}
} // namespace facebook::react::jsinspector_modern::tracing