Refactor RuntimeSamplingProfileTraceEventSerializer: use smart pointers, split code, add tests (#50531)

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

# Changelog: [Internal]

This should not have any functional changes, mostly refactoring the previous code, splitting it into smaller methods, better documentation and added tests.

Main changes are:
1. We are going to use smart pointers for `ProfileTreeNode`
2. Split single static method into multiple smaller methods of one class, less cognitive load and smaller context window

Reviewed By: robhogan

Differential Revision: D72401690

fbshipit-source-id: a1eed5501349dcd811661d91f3bf126a0daaacf2
This commit is contained in:
Ruslan Lesiutin
2025-04-08 09:40:50 -07:00
committed by Facebook GitHub Bot
parent 6609ba98e5
commit af5074509e
6 changed files with 649 additions and 204 deletions
@@ -72,7 +72,9 @@ bool TracingAgent::handleRequest(const cdp::PreparsedRequest& req) {
}
instanceAgent_->stopTracing();
bool correctlyStopped = PerformanceTracer::getInstance().stopTracing();
PerformanceTracer& performanceTracer = PerformanceTracer::getInstance();
bool correctlyStopped = performanceTracer.stopTracing();
if (!correctlyStopped) {
frontendChannel_(cdp::jsonError(
req.id,
@@ -90,15 +92,16 @@ bool TracingAgent::handleRequest(const cdp::PreparsedRequest& req) {
"Tracing.dataCollected",
folly::dynamic::object("value", eventsChunk)));
};
PerformanceTracer::getInstance().collectEvents(
performanceTracer.collectEvents(
dataCollectedCallback, TRACE_EVENT_CHUNK_SIZE);
tracing::RuntimeSamplingProfileTraceEventSerializer::serializeAndNotify(
PerformanceTracer::getInstance(),
instanceAgent_->collectTracingProfile().getRuntimeSamplingProfile(),
instanceTracingStartTimestamp_,
tracing::RuntimeSamplingProfileTraceEventSerializer serializer(
performanceTracer,
dataCollectedCallback,
PROFILE_TRACE_EVENT_CHUNK_SIZE);
serializer.serializeAndNotify(
instanceAgent_->collectTracingProfile().getRuntimeSamplingProfile(),
instanceTracingStartTimestamp_);
frontendChannel_(cdp::jsonNotification(
"Tracing.tracingComplete",
@@ -7,7 +7,10 @@
#pragma once
#include "RuntimeSamplingProfile.h"
#include <memory>
#include <utility>
#include <jsinspector-modern/tracing/RuntimeSamplingProfile.h>
namespace facebook::react::jsinspector_modern::tracing {
@@ -28,7 +31,7 @@ class ProfileTreeNode {
ProfileTreeNode(
uint32_t id,
CodeType codeType,
ProfileTreeNode* parent,
std::shared_ptr<ProfileTreeNode> parent,
RuntimeSamplingProfile::SampleCallStackFrame callFrame)
: id_(id),
codeType_(codeType),
@@ -47,7 +50,7 @@ class ProfileTreeNode {
* \return pointer to the parent node, nullptr if this is the root node.
*/
ProfileTreeNode* getParent() const {
return parent_;
return parent_.get();
}
/**
@@ -58,12 +61,14 @@ class ProfileTreeNode {
}
/**
* Will only add unique child node. Returns pointer to the already existing
* child node, nullptr if the added child node is unique.
* Will only add unique child node.
* \return shared 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) {
std::shared_ptr<ProfileTreeNode> addChild(
std::shared_ptr<ProfileTreeNode> child) {
for (const auto& existingChild : children_) {
if (*existingChild == child.get()) {
return existingChild;
}
}
@@ -94,13 +99,13 @@ class ProfileTreeNode {
*/
CodeType codeType_;
/**
* Pointer to the parent node. Should be nullptr only for root node.
* Shared pointer to the parent node. Can be nullptr only for root node.
*/
ProfileTreeNode* parent_{nullptr};
std::shared_ptr<ProfileTreeNode> parent_;
/**
* Lst of pointers to children nodes.
* Lst of shared pointers to children nodes.
*/
std::vector<ProfileTreeNode*> children_{};
std::vector<std::shared_ptr<ProfileTreeNode>> children_;
/**
* Information about the corresponding call frame that is represented by this
* node.
@@ -16,6 +16,11 @@ namespace {
// future, once we support multiple VMs being sampled at the same time.
const uint16_t PROFILE_ID = 1;
/// Fallback script ID for artificial call frames, such as (root), (idle) or
/// (program). Required for emulating the payload in a format that is expected
/// by Chrome DevTools.
const uint32_t FALLBACK_SCRIPT_ID = 0;
uint64_t formatTimePointToUnixTimestamp(
std::chrono::steady_clock::time_point timestamp) {
return std::chrono::duration_cast<std::chrono::microseconds>(
@@ -24,7 +29,7 @@ uint64_t formatTimePointToUnixTimestamp(
}
TraceEventProfileChunk::CPUProfile::Node convertToTraceEventProfileNode(
ProfileTreeNode* node) {
std::shared_ptr<ProfileTreeNode> node) {
ProfileTreeNode* nodeParent = node->getParent();
const RuntimeSamplingProfile::SampleCallStackFrame& callFrame =
node->getCallFrame();
@@ -51,203 +56,218 @@ TraceEventProfileChunk::CPUProfile::Node convertToTraceEventProfileNode(
: std::nullopt};
}
void emitSingleProfileChunk(
PerformanceTracer& performanceTracer,
std::vector<folly::dynamic>& buffer,
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));
}
RuntimeSamplingProfile::SampleCallStackFrame createArtificialCallFrame(
std::string callFrameName) {
return RuntimeSamplingProfile::SampleCallStackFrame{
RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction,
FALLBACK_SCRIPT_ID,
std::move(callFrameName)};
};
buffer.push_back(performanceTracer.getSerializedRuntimeProfileChunkTraceEvent(
profileId,
threadId,
chunkTimestamp,
TraceEventProfileChunk{
TraceEventProfileChunk::CPUProfile{traceEventNodes, samples},
TraceEventProfileChunk::TimeDeltas{timeDeltas},
}));
RuntimeSamplingProfile::SampleCallStackFrame createGarbageCollectorCallFrame() {
return RuntimeSamplingProfile::SampleCallStackFrame{
RuntimeSamplingProfile::SampleCallStackFrame::Kind::GarbageCollector,
FALLBACK_SCRIPT_ID,
"(garbage collector)"};
};
std::shared_ptr<ProfileTreeNode> createNode(
NodeIdGenerator& nodeIdGenerator,
ProfileTreeNode::CodeType codeType,
std::shared_ptr<ProfileTreeNode> parent,
const RuntimeSamplingProfile::SampleCallStackFrame& callFrame) {
return std::make_shared<ProfileTreeNode>(
nodeIdGenerator.getNext(), codeType, parent, callFrame);
}
std::shared_ptr<ProfileTreeNode> createArtificialNode(
NodeIdGenerator& nodeIdGenerator,
std::shared_ptr<ProfileTreeNode> parent,
std::string callFrameName) {
RuntimeSamplingProfile::SampleCallStackFrame callFrame =
createArtificialCallFrame(std::move(callFrameName));
return createNode(
nodeIdGenerator, ProfileTreeNode::CodeType::Other, parent, callFrame);
}
std::shared_ptr<ProfileTreeNode> createRootNode(
NodeIdGenerator& nodeIdGenerator) {
return createArtificialNode(nodeIdGenerator, nullptr, "(root)");
}
std::shared_ptr<ProfileTreeNode> createProgramNode(
NodeIdGenerator& nodeIdGenerator,
std::shared_ptr<ProfileTreeNode> rootNode) {
return createArtificialNode(nodeIdGenerator, rootNode, "(program)");
}
std::shared_ptr<ProfileTreeNode> createIdleNode(
NodeIdGenerator& nodeIdGenerator,
std::shared_ptr<ProfileTreeNode> rootNode) {
return createArtificialNode(nodeIdGenerator, rootNode, "(idle)");
}
} // namespace
/* static */ void
RuntimeSamplingProfileTraceEventSerializer::serializeAndNotify(
PerformanceTracer& performanceTracer,
void RuntimeSamplingProfileTraceEventSerializer::sendProfileTraceEvent(
uint64_t threadId,
uint16_t profileId,
uint64_t profileStartUnixTimestamp) const {
folly::dynamic serializedTraceEvent =
performanceTracer_.getSerializedRuntimeProfileTraceEvent(
threadId, profileId, profileStartUnixTimestamp);
notificationCallback_(folly::dynamic::array(serializedTraceEvent));
}
void RuntimeSamplingProfileTraceEventSerializer::chunkEmptySample(
ProfileChunk& chunk,
uint32_t idleNodeId,
long long samplesTimeDelta) {
chunk.samples.push_back(idleNodeId);
chunk.timeDeltas.push_back(samplesTimeDelta);
}
void RuntimeSamplingProfileTraceEventSerializer::bufferProfileChunkTraceEvent(
ProfileChunk& chunk,
uint16_t profileId) {
if (chunk.isEmpty()) {
return;
}
std::vector<TraceEventProfileChunk::CPUProfile::Node> traceEventNodes;
traceEventNodes.reserve(chunk.nodes.size());
for (const auto& node : chunk.nodes) {
traceEventNodes.push_back(convertToTraceEventProfileNode(node));
}
traceEventBuffer_.push_back(
performanceTracer_.getSerializedRuntimeProfileChunkTraceEvent(
profileId,
chunk.threadId,
chunk.timestamp,
TraceEventProfileChunk{
TraceEventProfileChunk::CPUProfile{
traceEventNodes, chunk.samples},
TraceEventProfileChunk::TimeDeltas{chunk.timeDeltas},
}));
}
void RuntimeSamplingProfileTraceEventSerializer::processCallStack(
const std::vector<RuntimeSamplingProfile::SampleCallStackFrame>& callStack,
ProfileChunk& chunk,
std::shared_ptr<ProfileTreeNode> rootNode,
std::shared_ptr<ProfileTreeNode> idleNode,
long long samplesTimeDelta,
NodeIdGenerator& nodeIdGenerator) {
if (callStack.empty()) {
chunkEmptySample(chunk, idleNode->getId(), samplesTimeDelta);
return;
}
auto previousNode = rootNode;
for (auto it = callStack.rbegin(); it != callStack.rend(); ++it) {
const RuntimeSamplingProfile::SampleCallStackFrame& 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 = createNode(
nodeIdGenerator,
isGarbageCollectorFrame ? ProfileTreeNode::CodeType::Other
: ProfileTreeNode::CodeType::JavaScript,
previousNode,
isGarbageCollectorFrame ? createGarbageCollectorCallFrame()
: callFrame);
auto alreadyExistingNode = previousNode->addChild(currentNode);
if (alreadyExistingNode != nullptr) {
previousNode = alreadyExistingNode;
} else {
chunk.nodes.push_back(currentNode);
previousNode = currentNode;
}
}
chunk.samples.push_back(previousNode->getId());
chunk.timeDeltas.push_back(samplesTimeDelta);
}
void RuntimeSamplingProfileTraceEventSerializer::
sendBufferedTraceEventsAndClear() {
notificationCallback_(traceEventBuffer_);
traceEventBuffer_ = folly::dynamic::array();
}
void RuntimeSamplingProfileTraceEventSerializer::serializeAndNotify(
const RuntimeSamplingProfile& profile,
std::chrono::steady_clock::time_point tracingStartTime,
const std::function<void(const folly::dynamic& traceEventsChunk)>&
notificationCallback,
uint16_t traceEventChunkSize,
uint16_t profileChunkSize) {
std::chrono::steady_clock::time_point tracingStartTime) {
const std::vector<RuntimeSamplingProfile::Sample>& samples =
profile.getSamples();
if (samples.empty()) {
return;
}
std::vector<folly::dynamic> buffer;
uint64_t chunkThreadId = samples.front().getThreadId();
uint64_t firstChunkThreadId = samples.front().getThreadId();
uint64_t tracingStartUnixTimestamp =
formatTimePointToUnixTimestamp(tracingStartTime);
buffer.push_back(performanceTracer.getSerializedRuntimeProfileTraceEvent(
chunkThreadId, PROFILE_ID, tracingStartUnixTimestamp));
uint64_t previousSampleUnixTimestamp = tracingStartUnixTimestamp;
uint64_t currentChunkUnixTimestamp = 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)"});
sendProfileTraceEvent(
firstChunkThreadId, PROFILE_ID, tracingStartUnixTimestamp);
NodeIdGenerator nodeIdGenerator{};
auto rootNode = createRootNode(nodeIdGenerator);
auto programNode = createProgramNode(nodeIdGenerator, rootNode);
auto idleNode = createIdleNode(nodeIdGenerator, rootNode);
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);
ProfileChunk chunk{
profileChunkSize_, firstChunkThreadId, currentChunkUnixTimestamp};
chunk.nodes.push_back(rootNode);
chunk.nodes.push_back(programNode);
chunk.nodes.push_back(idleNode);
std::vector<uint32_t> samplesInThisChunk;
samplesInThisChunk.reserve(profileChunkSize);
std::vector<long long> timeDeltasInThisChunk;
timeDeltasInThisChunk.reserve(profileChunkSize);
for (const auto& sample : samples) {
uint64_t currentSampleThreadId = sample.getThreadId();
long long currentSampleUnixTimestamp = sample.getTimestamp();
RuntimeSamplingProfile::SampleCallStackFrame garbageCollectorCallFrame =
RuntimeSamplingProfile::SampleCallStackFrame{
RuntimeSamplingProfile::SampleCallStackFrame::Kind::GarbageCollector,
0,
"(garbage collector)"};
uint64_t chunkTimestamp = tracingStartUnixTimestamp;
for (const RuntimeSamplingProfile::Sample& sample : samples) {
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,
buffer,
PROFILE_ID,
chunkThreadId,
chunkTimestamp,
nodesInThisChunk,
samplesInThisChunk,
timeDeltasInThisChunk);
nodesInThisChunk.clear();
samplesInThisChunk.clear();
timeDeltasInThisChunk.clear();
// We should not attempt to merge samples from different threads.
// From past observations, this only happens for GC nodes.
// We should group samples by thread id once we support executing JavaScript
// on different threads.
if (currentSampleThreadId != chunk.threadId || chunk.isFull()) {
bufferProfileChunkTraceEvent(chunk, PROFILE_ID);
chunk = ProfileChunk{
profileChunkSize_, currentSampleThreadId, currentChunkUnixTimestamp};
}
chunkThreadId = sampleThreadId;
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack =
sample.getCallStack();
uint64_t sampleTimestamp = sample.getTimestamp();
if (samplesInThisChunk.empty()) {
// New chunk. Reset the timestamp.
chunkTimestamp = sampleTimestamp;
if (traceEventBuffer_.size() == traceEventChunkSize_) {
sendBufferedTraceEventsAndClear();
}
long long timeDelta = sampleTimestamp - previousSampleTimestamp;
timeDeltasInThisChunk.push_back(timeDelta);
previousSampleTimestamp = sampleTimestamp;
processCallStack(
sample.getCallStack(),
chunk,
rootNode,
idleNode,
currentSampleUnixTimestamp - previousSampleUnixTimestamp,
nodeIdGenerator);
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,
buffer,
PROFILE_ID,
chunkThreadId,
chunkTimestamp,
nodesInThisChunk,
samplesInThisChunk,
timeDeltasInThisChunk);
nodesInThisChunk.clear();
samplesInThisChunk.clear();
timeDeltasInThisChunk.clear();
}
if (buffer.size() == traceEventChunkSize) {
notificationCallback(folly::dynamic::array(buffer.begin(), buffer.end()));
buffer.clear();
}
previousSampleUnixTimestamp = currentSampleUnixTimestamp;
}
if (!samplesInThisChunk.empty()) {
emitSingleProfileChunk(
performanceTracer,
buffer,
PROFILE_ID,
chunkThreadId,
chunkTimestamp,
nodesInThisChunk,
samplesInThisChunk,
timeDeltasInThisChunk);
if (!chunk.isEmpty()) {
bufferProfileChunkTraceEvent(chunk, PROFILE_ID);
}
if (!buffer.empty()) {
notificationCallback(folly::dynamic::array(buffer.begin(), buffer.end()));
buffer.clear();
if (!traceEventBuffer_.empty()) {
sendBufferedTraceEventsAndClear();
}
}
@@ -8,39 +8,157 @@
#pragma once
#include "PerformanceTracer.h"
#include "ProfileTreeNode.h"
#include "RuntimeSamplingProfile.h"
namespace facebook::react::jsinspector_modern::tracing {
namespace {
struct NodeIdGenerator {
public:
uint32_t getNext() {
return ++counter_;
}
private:
uint32_t counter_ = 0;
};
} // namespace
/**
* Serializes RuntimeSamplingProfile into collection of specific Trace Events,
* which represent Profile information on a timeline.
*/
class RuntimeSamplingProfileTraceEventSerializer {
public:
RuntimeSamplingProfileTraceEventSerializer() = delete;
struct ProfileChunk {
ProfileChunk(
uint16_t chunkSize,
uint64_t chunkThreadId,
uint64_t chunkTimestamp)
: size(chunkSize), threadId(chunkThreadId), timestamp(chunkTimestamp) {
samples.reserve(size);
timeDeltas.reserve(size);
}
bool isFull() const {
return samples.size() == size;
}
bool isEmpty() const {
return samples.empty();
}
std::vector<std::shared_ptr<ProfileTreeNode>> nodes;
std::vector<uint32_t> samples;
std::vector<long long> timeDeltas;
uint16_t size;
uint64_t threadId;
uint64_t timestamp;
};
public:
/**
* \param performanceTracer A reference to PerformanceTracer instance.
* \param profile What we will be serializing.
* \param tracingStartTime A timestamp of when tracing of an
* Instance started, will be used as a starting reference point of JavaScript
* samples recording.
* \param notificationCallback A callback, which is called
* \param notificationCallback A reference to a callback, which is called
* when a chunk of trace events is ready to be sent.
* \param traceEventChunkSize The maximum number of ProfileChunk trace events
* that can be sent in a single CDP Tracing.dataCollected message.
* \param traceEventChunkSize The maximum number of ProfileChunk trace
* events that can be sent in a single CDP Tracing.dataCollected message.
* \param profileChunkSize The maximum number of ProfileChunk trace events
* that can be sent in a single ProfileChunk trace event.
*/
static void serializeAndNotify(
RuntimeSamplingProfileTraceEventSerializer(
PerformanceTracer& performanceTracer,
const RuntimeSamplingProfile& profile,
std::chrono::steady_clock::time_point tracingStartTime,
const std::function<void(const folly::dynamic& traceEventsChunk)>&
std::function<void(const folly::dynamic& traceEventsChunk)>
notificationCallback,
uint16_t traceEventChunkSize,
uint16_t profileChunkSize = 10);
uint16_t profileChunkSize = 10)
: performanceTracer_(performanceTracer),
notificationCallback_(std::move(notificationCallback)),
traceEventChunkSize_(traceEventChunkSize),
profileChunkSize_(profileChunkSize) {
traceEventBuffer_ = folly::dynamic::array();
traceEventBuffer_.reserve(traceEventChunkSize);
}
/**
* \param profile What we will be serializing.
* \param tracingStartTime A timestamp of when tracing of an Instance started,
* will be used as a starting reference point of JavaScript samples recording.
*/
void serializeAndNotify(
const RuntimeSamplingProfile& profile,
std::chrono::steady_clock::time_point tracingStartTime);
private:
/**
* Sends a single "Profile" Trace Event via notificationCallback_.
* \param threadId The id of the thread, where the Profile was collected.
* \param profileId The id of the Profile.
* \param profileStartUnixTimestamp The Unix timestamp of the start of the
* profile.
*/
void sendProfileTraceEvent(
uint64_t threadId,
uint16_t profileId,
uint64_t profileStartUnixTimestamp) const;
/**
* Encapsulates logic for processing the empty sample, when the VM was idling.
* \param chunk The profile chunk, which will record this sample.
* \param idleNodeId The id of the (idle) node.
* \param samplesTimeDelta Delta between the current sample and the
* previous one.
*/
void chunkEmptySample(
ProfileChunk& chunk,
uint32_t idleNodeId,
long long samplesTimeDelta);
/**
* Records ProfileChunk as a "ProfileChunk" Trace Event in traceEventBuffer_.
* \param chunk The chunk that will be buffered.
* \param profileId The id of the Profile.
*/
void bufferProfileChunkTraceEvent(ProfileChunk& chunk, uint16_t profileId);
/**
* Encapsulates logic for processing the call stack of the sample.
* \param callStack The call stack that will be processed.
* \param chunk The profile chunk, which will buffer the sample with the
* provided call stack.
* \param rootNode Shared pointer to the (root) node. Will be the parent node
* of the corresponding profile tree branch.
* \param idleNode Shared pointer to the (idle) node. Will be the only node
* that is used for the corresponding profile tree branch, in case of an empty
* call stack.
* \param samplesTimeDelta Delta between the current sample and the previous
* one.
* \param nodeIdGenerator NodeIdGenerator instance that will be used for
* generating unique node ids.
*/
void processCallStack(
const std::vector<RuntimeSamplingProfile::SampleCallStackFrame>&
callStack,
ProfileChunk& chunk,
std::shared_ptr<ProfileTreeNode> rootNode,
std::shared_ptr<ProfileTreeNode> idleNode,
long long samplesTimeDelta,
NodeIdGenerator& nodeIdGenerator);
/**
* Sends buffered Trace Events via notificationCallback_ and then clears it.
*/
void sendBufferedTraceEventsAndClear();
PerformanceTracer& performanceTracer_;
const std::function<void(const folly::dynamic& traceEventsChunk)>
notificationCallback_;
uint16_t traceEventChunkSize_;
uint16_t profileChunkSize_;
folly::dynamic traceEventBuffer_;
};
} // namespace facebook::react::jsinspector_modern::tracing
@@ -25,30 +25,39 @@ TEST(ProfileTreeNodeTest, EqualityOperator) {
EXPECT_EQ(*node1 == node2, true);
node1 = new ProfileTreeNode(
3, ProfileTreeNode::CodeType::JavaScript, node1, callFrame);
3,
ProfileTreeNode::CodeType::JavaScript,
std::shared_ptr<ProfileTreeNode>(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);
5,
ProfileTreeNode::CodeType::JavaScript,
std::shared_ptr<ProfileTreeNode>(node2),
callFrame);
node2 = new ProfileTreeNode(
6, ProfileTreeNode::CodeType::JavaScript, node2, callFrame);
6,
ProfileTreeNode::CodeType::JavaScript,
std::shared_ptr<ProfileTreeNode>(node2),
callFrame);
EXPECT_EQ(*node1 == node2, true);
}
TEST(ProfileTreeNodeTest, OnlyAddsUniqueChildren) {
auto callFrame = RuntimeSamplingProfile::SampleCallStackFrame{
RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction, 0, "foo"};
ProfileTreeNode* parent = new ProfileTreeNode(
auto parent = std::make_shared<ProfileTreeNode>(
1, ProfileTreeNode::CodeType::JavaScript, nullptr, callFrame);
ProfileTreeNode* existingChild = new ProfileTreeNode(
auto existingChild = std::make_shared<ProfileTreeNode>(
2, ProfileTreeNode::CodeType::JavaScript, parent, callFrame);
auto maybeAlreadyExistingChild = parent->addChild(existingChild);
EXPECT_EQ(maybeAlreadyExistingChild, nullptr);
auto copyOfExistingChild = new ProfileTreeNode(
auto copyOfExistingChild = std::make_shared<ProfileTreeNode>(
3, ProfileTreeNode::CodeType::JavaScript, parent, callFrame);
maybeAlreadyExistingChild = parent->addChild(copyOfExistingChild);
@@ -0,0 +1,290 @@
/*
* 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 <jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <chrono>
#include <utility>
namespace facebook::react::jsinspector_modern::tracing {
class RuntimeSamplingProfileTraceEventSerializerTest : public ::testing::Test {
protected:
std::vector<folly::dynamic> notificationEvents_;
std::function<void(const folly::dynamic& traceEventsChunk)>
createNotificationCallback() {
return [this](const folly::dynamic& traceEventsChunk) {
notificationEvents_.push_back(traceEventsChunk);
};
}
RuntimeSamplingProfile::SampleCallStackFrame createJSCallFrame(
std::string functionName,
uint32_t scriptId = 1,
std::optional<std::string> url = std::nullopt,
std::optional<uint32_t> lineNumber = std::nullopt,
std::optional<uint32_t> columnNumber = std::nullopt) {
return RuntimeSamplingProfile::SampleCallStackFrame(
RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction,
scriptId,
std::move(functionName),
std::move(url),
lineNumber,
columnNumber);
}
RuntimeSamplingProfile::SampleCallStackFrame createGCCallFrame() {
return RuntimeSamplingProfile::SampleCallStackFrame(
RuntimeSamplingProfile::SampleCallStackFrame::Kind::GarbageCollector,
0,
"(garbage collector)");
}
RuntimeSamplingProfile::Sample createSample(
uint64_t timestamp,
uint64_t threadId,
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack) {
return {timestamp, threadId, std::move(callStack)};
}
RuntimeSamplingProfile createEmptyProfile() {
return {"TestRuntime", {}};
}
RuntimeSamplingProfile createProfileWithSamples(
std::vector<RuntimeSamplingProfile::Sample> samples) {
return {"TestRuntime", std::move(samples)};
}
};
TEST_F(RuntimeSamplingProfileTraceEventSerializerTest, EmptyProfile) {
// Setup
auto notificationCallback = createNotificationCallback();
RuntimeSamplingProfileTraceEventSerializer serializer(
PerformanceTracer::getInstance(), notificationCallback, 10);
auto profile = createEmptyProfile();
auto tracingStartTime = std::chrono::steady_clock::now();
// Execute
serializer.serializeAndNotify(profile, tracingStartTime);
// Nothing should be reported if the profile is empty.
EXPECT_TRUE(notificationEvents_.empty());
}
TEST_F(
RuntimeSamplingProfileTraceEventSerializerTest,
SameCallFramesAreMerged) {
// Setup
auto notificationCallback = createNotificationCallback();
RuntimeSamplingProfileTraceEventSerializer serializer(
PerformanceTracer::getInstance(), notificationCallback, 10);
// [ foo ]
// [ bar ]
// [baz][(gc)]
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack1 = {
createJSCallFrame("bar", 1, "test.js", 20, 10),
createJSCallFrame("foo", 1, "test.js", 10, 5),
};
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack2 = {
createJSCallFrame("baz", 1, "other.js", 5, 1),
createJSCallFrame("bar", 1, "test.js", 20, 10),
createJSCallFrame("foo", 1, "test.js", 10, 5),
};
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack3 = {
createGCCallFrame(),
createJSCallFrame("bar", 1, "test.js", 20, 10),
createJSCallFrame("foo", 1, "test.js", 10, 5),
};
uint64_t threadId = 1;
uint64_t timestamp1 = 1000000;
uint64_t timestamp2 = 2000000;
uint64_t timestamp3 = 3000000;
auto samples = std::vector<RuntimeSamplingProfile::Sample>{};
samples.emplace_back(createSample(timestamp1, threadId, callStack1));
samples.emplace_back(createSample(timestamp2, threadId, callStack2));
samples.emplace_back(createSample(timestamp3, threadId, callStack3));
auto profile = createProfileWithSamples(std::move(samples));
auto tracingStartTime = std::chrono::steady_clock::now();
// Execute
serializer.serializeAndNotify(profile, tracingStartTime);
// Verify
ASSERT_EQ(notificationEvents_.size(), 2);
// (root), (program), (idle), foo, bar, baz, (garbage collector)
ASSERT_EQ(
notificationEvents_[1][0]["args"]["data"]["cpuProfile"]["nodes"].size(),
7);
}
TEST_F(RuntimeSamplingProfileTraceEventSerializerTest, EmptySample) {
// Setup
auto notificationCallback = createNotificationCallback();
RuntimeSamplingProfileTraceEventSerializer serializer(
PerformanceTracer::getInstance(), notificationCallback, 10);
// Create an empty sample (no call stack)
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> emptyCallStack;
uint64_t threadId = 1;
uint64_t timestamp = 1000000;
auto samples = std::vector<RuntimeSamplingProfile::Sample>{};
samples.emplace_back(createSample(timestamp, threadId, emptyCallStack));
auto profile = createProfileWithSamples(std::move(samples));
auto tracingStartTime = std::chrono::steady_clock::now();
// Mock the performance tracer methods
folly::dynamic profileEvent = folly::dynamic::object;
folly::dynamic chunkEvent = folly::dynamic::object;
// Execute
serializer.serializeAndNotify(profile, tracingStartTime);
// Verify
// [["Profile"], ["ProfileChunk"]]
ASSERT_EQ(notificationEvents_.size(), 2);
// (root), (program), (idle)
ASSERT_EQ(
notificationEvents_[1][0]["args"]["data"]["cpuProfile"]["nodes"].size(),
3);
}
TEST_F(
RuntimeSamplingProfileTraceEventSerializerTest,
SamplesFromDifferentThreads) {
// Setup
auto notificationCallback = createNotificationCallback();
RuntimeSamplingProfileTraceEventSerializer serializer(
PerformanceTracer::getInstance(), notificationCallback, 10);
// Create samples with different thread IDs
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack = {
createJSCallFrame("foo", 1, "test.js", 10, 5)};
uint64_t timestamp = 1000000;
uint64_t threadId1 = 1;
uint64_t threadId2 = 2;
auto samples = std::vector<RuntimeSamplingProfile::Sample>{};
samples.emplace_back(createSample(timestamp, threadId1, callStack));
samples.emplace_back(createSample(timestamp + 1000, threadId2, callStack));
samples.emplace_back(createSample(timestamp + 2000, threadId1, callStack));
auto profile = createProfileWithSamples(std::move(samples));
auto tracingStartTime = std::chrono::steady_clock::now();
// Execute
serializer.serializeAndNotify(profile, tracingStartTime);
// [["Profile"], ["ProfileChunk", "ProfileChunk", "ProfileChunk]]
// Samples from different thread should never be grouped together in the same
// chunk.
ASSERT_EQ(notificationEvents_.size(), 2);
ASSERT_EQ(notificationEvents_[1].size(), 3);
}
TEST_F(
RuntimeSamplingProfileTraceEventSerializerTest,
TraceEventChunkSizeLimit) {
// Setup
auto notificationCallback = createNotificationCallback();
uint16_t traceEventChunkSize = 2;
uint16_t profileChunkSize = 2;
RuntimeSamplingProfileTraceEventSerializer serializer(
PerformanceTracer::getInstance(),
notificationCallback,
traceEventChunkSize,
profileChunkSize);
// Create multiple samples
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack = {
createJSCallFrame("foo", 1, "test.js", 10, 5)};
uint64_t timestamp = 1000000;
uint64_t threadId = 1;
std::vector<RuntimeSamplingProfile::Sample> samples;
samples.reserve(5);
for (int i = 0; i < 5; i++) {
samples.push_back(createSample(timestamp + i * 1000, threadId, callStack));
}
auto profile = createProfileWithSamples(std::move(samples));
auto tracingStartTime = std::chrono::steady_clock::now();
// Execute
serializer.serializeAndNotify(profile, tracingStartTime);
// [["Profile"], ["ProfileChunk", "ProfileChunk"], ["ProfileChunk"]]
ASSERT_EQ(notificationEvents_.size(), 3);
// Check that each chunk has at most traceEventChunkSize events
for (size_t i = 1; i < notificationEvents_.size(); i++) {
EXPECT_LE(notificationEvents_[i].size(), traceEventChunkSize);
}
}
TEST_F(RuntimeSamplingProfileTraceEventSerializerTest, ProfileChunkSizeLimit) {
// Setup
auto notificationCallback = createNotificationCallback();
// Set a small profile chunk size to test profile chunking
uint16_t traceEventChunkSize = 10;
uint16_t profileChunkSize = 2;
double samplesCount = 5;
RuntimeSamplingProfileTraceEventSerializer serializer(
PerformanceTracer::getInstance(),
notificationCallback,
traceEventChunkSize,
profileChunkSize);
// Create multiple samples
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack = {
createJSCallFrame("foo", 1, "test.js", 10, 5)};
uint64_t timestamp = 1000000;
uint64_t threadId = 1;
std::vector<RuntimeSamplingProfile::Sample> samples;
samples.reserve(samplesCount);
for (int i = 0; i < samplesCount; i++) {
samples.push_back(createSample(timestamp + i * 1000, threadId, callStack));
}
auto profile = createProfileWithSamples(std::move(samples));
auto tracingStartTime = std::chrono::steady_clock::now();
// Execute
serializer.serializeAndNotify(profile, tracingStartTime);
// [["Profile"], ["ProfileChunk", "ProfileChunk", "ProfileChunk"]]
ASSERT_EQ(notificationEvents_.size(), 2);
ASSERT_EQ(
notificationEvents_[1].size(),
std::ceil(samplesCount / profileChunkSize));
for (auto& profileChunk : notificationEvents_[1]) {
EXPECT_LE(
profileChunk["args"]["data"]["cpuProfile"]["samples"].size(),
profileChunkSize);
}
}
} // namespace facebook::react::jsinspector_modern::tracing