refactor RuntimeSamplingProfile serializer (#52916)

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

# Changelog: [Internal]

Align with other serializers in `jsinspector-modern` to have just static public method.

Reviewed By: rubennorte

Differential Revision: D79131985

fbshipit-source-id: 3f1f08641bb96a9fbd067992b8ce294af9d27688
This commit is contained in:
Ruslan Lesiutin
2025-07-31 07:43:31 -07:00
committed by Facebook GitHub Bot
parent a8f6c96bc2
commit b58c2ffd72
4 changed files with 182 additions and 198 deletions
@@ -110,12 +110,14 @@ bool TracingAgent::handleRequest(const cdp::PreparsedRequest& req) {
performanceTracer.collectEvents(
dataCollectedCallback, TRACE_EVENT_CHUNK_SIZE);
tracing::RuntimeSamplingProfileTraceEventSerializer serializer(
dataCollectedCallback, PROFILE_TRACE_EVENT_CHUNK_SIZE);
auto tracingProfile = instanceAgent_->collectTracingProfile();
serializer.serializeAndNotify(
tracing::IdGenerator profileIdGenerator;
tracing::RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
std::move(tracingProfile.runtimeSamplingProfile),
instanceTracingStartTimestamp_);
profileIdGenerator,
instanceTracingStartTimestamp_,
dataCollectedCallback,
PROFILE_TRACE_EVENT_CHUNK_SIZE);
frontendChannel_(cdp::jsonNotification(
"Tracing.tracingComplete",
@@ -27,10 +27,6 @@ HighResTimeStamp getHighResTimeStampForSample(
return HighResTimeStamp::fromChronoSteadyClockTimePoint(chronoTimePoint);
}
// Right now we only emit single Profile. We might revisit this decision in the
// future, once we support multiple VMs being sampled at the same time.
constexpr RuntimeProfileId 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.
@@ -62,7 +58,7 @@ TraceEventProfileChunk::CPUProfile::Node convertToTraceEventProfileNode(
return TraceEventProfileChunk::CPUProfile::Node{
.id = node.getId(),
.callFrame = traceEventCallFrame,
.callFrame = std::move(traceEventCallFrame),
.parentId = node.hasParent() ? std::optional<uint32_t>(node.getParentId())
: std::nullopt,
};
@@ -95,22 +91,55 @@ class ProfileTreeRootNode : public ProfileTreeNode {
createArtificialCallFrame(ROOT_FRAME_NAME)) {}
};
} // namespace
struct ProfileChunk {
ProfileChunk(
uint16_t chunkSize,
ProcessId chunkProcessId,
ThreadId chunkThreadId,
HighResTimeStamp chunkTimestamp)
: size(chunkSize),
processId(chunkProcessId),
threadId(chunkThreadId),
timestamp(chunkTimestamp) {
samples.reserve(size);
timeDeltas.reserve(size);
}
void RuntimeSamplingProfileTraceEventSerializer::sendProfileTraceEvent(
inline bool isFull() const {
return samples.size() == size;
}
inline bool isEmpty() const {
return samples.empty();
}
std::vector<ProfileTreeNode> nodes;
std::vector<uint32_t> samples;
std::vector<HighResDuration> timeDeltas;
uint16_t size;
ProcessId processId;
ThreadId threadId;
HighResTimeStamp timestamp;
};
// Construct and send "Profile" Trace Event with dispatchCallback.
void sendProfileTraceEvent(
ProcessId processId,
ThreadId threadId,
RuntimeProfileId profileId,
HighResTimeStamp profileStartTimestamp) const {
HighResTimeStamp profileStartTimestamp,
const std::function<void(folly::dynamic&& traceEventsChunk)>&
dispatchCallback) {
auto traceEvent = PerformanceTracer::constructRuntimeProfileTraceEvent(
profileId, processId, threadId, profileStartTimestamp);
folly::dynamic serializedTraceEvent =
TraceEventSerializer::serialize(std::move(traceEvent));
notificationCallback_(folly::dynamic::array(std::move(serializedTraceEvent)));
dispatchCallback(folly::dynamic::array(std::move(serializedTraceEvent)));
}
void RuntimeSamplingProfileTraceEventSerializer::chunkEmptySample(
// Add an empty sample to the chunk.
void chunkEmptySample(
ProfileChunk& chunk,
uint32_t idleNodeId,
HighResDuration samplesTimeDelta) {
@@ -118,13 +147,12 @@ void RuntimeSamplingProfileTraceEventSerializer::chunkEmptySample(
chunk.timeDeltas.push_back(samplesTimeDelta);
}
void RuntimeSamplingProfileTraceEventSerializer::bufferProfileChunkTraceEvent(
// Take the current local ProfileChunk, serialize it as "ProfileChunk" Trace
// Event and buffer it.
void bufferProfileChunkTraceEvent(
ProfileChunk&& chunk,
RuntimeProfileId profileId) {
if (chunk.isEmpty()) {
return;
}
RuntimeProfileId profileId,
folly::dynamic& traceEventBuffer) {
std::vector<TraceEventProfileChunk::CPUProfile::Node> traceEventNodes;
traceEventNodes.reserve(chunk.nodes.size());
for (const auto& node : chunk.nodes) {
@@ -146,16 +174,17 @@ void RuntimeSamplingProfileTraceEventSerializer::bufferProfileChunkTraceEvent(
auto serializedTraceEvent =
TraceEventSerializer::serialize(std::move(traceEvent));
traceEventBuffer_.push_back(std::move(serializedTraceEvent));
traceEventBuffer.push_back(std::move(serializedTraceEvent));
}
void RuntimeSamplingProfileTraceEventSerializer::processCallStack(
// Process a call stack of a single sample and add it to the chunk.
void processCallStack(
std::vector<RuntimeSamplingProfile::SampleCallStackFrame>&& callStack,
ProfileChunk& chunk,
ProfileTreeNode& rootNode,
uint32_t idleNodeId,
HighResDuration samplesTimeDelta,
NodeIdGenerator& nodeIdGenerator) {
IdGenerator& nodeIdGenerator) {
if (callStack.empty()) {
chunkEmptySample(chunk, idleNodeId, samplesTimeDelta);
return;
@@ -190,39 +219,71 @@ void RuntimeSamplingProfileTraceEventSerializer::processCallStack(
chunk.timeDeltas.push_back(samplesTimeDelta);
}
void RuntimeSamplingProfileTraceEventSerializer::
sendBufferedTraceEventsAndClear() {
notificationCallback_(std::move(traceEventBuffer_));
traceEventBuffer_ = folly::dynamic::array();
traceEventBuffer_.reserve(traceEventChunkSize_);
// Send buffered Trace Events and reset the buffer.
void sendBufferedTraceEvents(
folly::dynamic&& traceEventBuffer,
const std::function<void(folly::dynamic&& traceEventsChunk)>&
dispatchCallback) {
dispatchCallback(std::move(traceEventBuffer));
}
void RuntimeSamplingProfileTraceEventSerializer::serializeAndNotify(
} // namespace
/* static */ void
RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
std::vector<RuntimeSamplingProfile>&& profiles,
IdGenerator& profileIdGenerator,
HighResTimeStamp tracingStartTime,
const std::function<void(folly::dynamic&& traceEventsChunk)>&
dispatchCallback,
uint16_t traceEventChunkSize,
uint16_t profileChunkSize) {
for (auto&& profile : profiles) {
serializeAndDispatch(
std::move(profile),
profileIdGenerator,
tracingStartTime,
dispatchCallback,
traceEventChunkSize,
profileChunkSize);
}
}
/* static */ void
RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
RuntimeSamplingProfile&& profile,
HighResTimeStamp tracingStartTime) {
IdGenerator& profileIdGenerator,
HighResTimeStamp tracingStartTime,
const std::function<void(folly::dynamic&& traceEventsChunk)>&
dispatchCallback,
uint16_t traceEventChunkSize,
uint16_t profileChunkSize) {
auto samples = std::move(profile.samples);
if (samples.empty()) {
return;
}
ThreadId firstChunkThreadId = samples.front().threadId;
auto traceEventBuffer = folly::dynamic::array();
traceEventBuffer.reserve(traceEventChunkSize);
ThreadId threadId = samples.front().threadId;
HighResTimeStamp previousSampleTimestamp = tracingStartTime;
HighResTimeStamp currentChunkTimestamp = tracingStartTime;
auto profileId = profileIdGenerator.getNext();
sendProfileTraceEvent(
profile.processId, firstChunkThreadId, PROFILE_ID, tracingStartTime);
profile.processId,
threadId,
profileId,
tracingStartTime,
dispatchCallback);
// There could be any number of new nodes in this chunk. Empty if all nodes
// are already emitted in previous chunks.
ProfileChunk chunk{
profileChunkSize_,
profile.processId,
firstChunkThreadId,
currentChunkTimestamp};
NodeIdGenerator nodeIdGenerator{};
profileChunkSize, profile.processId, threadId, currentChunkTimestamp};
IdGenerator nodeIdGenerator{};
ProfileTreeRootNode rootNode(nodeIdGenerator.getNext());
chunk.nodes.push_back(rootNode);
@@ -248,16 +309,20 @@ void RuntimeSamplingProfileTraceEventSerializer::serializeAndNotify(
// We should group samples by thread id once we support executing JavaScript
// on different threads.
if (currentSampleThreadId != chunk.threadId || chunk.isFull()) {
bufferProfileChunkTraceEvent(std::move(chunk), PROFILE_ID);
bufferProfileChunkTraceEvent(
std::move(chunk), profileId, traceEventBuffer);
chunk = ProfileChunk{
profileChunkSize_,
profileChunkSize,
profile.processId,
currentSampleThreadId,
currentChunkTimestamp};
}
if (traceEventBuffer_.size() == traceEventChunkSize_) {
sendBufferedTraceEventsAndClear();
if (traceEventBuffer.size() == traceEventChunkSize) {
sendBufferedTraceEvents(std::move(traceEventBuffer), dispatchCallback);
traceEventBuffer = folly::dynamic::array();
traceEventBuffer.reserve(traceEventChunkSize);
}
processCallStack(
@@ -272,11 +337,11 @@ void RuntimeSamplingProfileTraceEventSerializer::serializeAndNotify(
}
if (!chunk.isEmpty()) {
bufferProfileChunkTraceEvent(std::move(chunk), PROFILE_ID);
bufferProfileChunkTraceEvent(std::move(chunk), profileId, traceEventBuffer);
}
if (!traceEventBuffer_.empty()) {
sendBufferedTraceEventsAndClear();
if (!traceEventBuffer.empty()) {
sendBufferedTraceEvents(std::move(traceEventBuffer), dispatchCallback);
}
}
@@ -7,16 +7,13 @@
#pragma once
#include "ProfileTreeNode.h"
#include "RuntimeSamplingProfile.h"
#include <react/timing/primitives.h>
namespace facebook::react::jsinspector_modern::tracing {
namespace {
struct NodeIdGenerator {
struct IdGenerator {
public:
uint32_t getNext() {
return ++counter_;
@@ -26,144 +23,42 @@ struct NodeIdGenerator {
uint32_t counter_ = 0;
};
} // namespace
/**
* Serializes RuntimeSamplingProfile into collection of specific Trace Events,
* which represent Profile information on a timeline.
*/
class RuntimeSamplingProfileTraceEventSerializer {
struct ProfileChunk {
ProfileChunk(
uint16_t chunkSize,
ProcessId chunkProcessId,
ThreadId chunkThreadId,
HighResTimeStamp chunkTimestamp)
: size(chunkSize),
processId(chunkProcessId),
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<ProfileTreeNode> nodes;
std::vector<uint32_t> samples;
std::vector<HighResDuration> timeDeltas;
uint16_t size;
ProcessId processId;
ThreadId threadId;
HighResTimeStamp timestamp;
};
public:
/**
* \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 profile What we will be serializing.
* \param profileIdGenerator A reference to an IdGenerator, which will be
* used for generating unique ids for ProfileChunks.
* \param tracingStartTime A timestamp of when tracing started, will be used
* as a starting reference point of JavaScript samples recording.
* \param dispatchCallback 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 profileChunkSize The maximum number of ProfileChunk trace events
* that can be sent in a single ProfileChunk trace event.
*/
RuntimeSamplingProfileTraceEventSerializer(
std::function<void(folly::dynamic&& traceEventsChunk)>
notificationCallback,
uint16_t traceEventChunkSize,
uint16_t profileChunkSize = 10)
: 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(
static void serializeAndDispatch(
RuntimeSamplingProfile&& profile,
HighResTimeStamp tracingStartTime);
IdGenerator& profileIdGenerator,
HighResTimeStamp tracingStartTime,
const std::function<void(folly::dynamic&& traceEventsChunk)>&
dispatchCallback,
uint16_t traceEventChunkSize,
uint16_t profileChunkSize = 10);
private:
/**
* Sends a single "Profile" Trace Event via notificationCallback_.
* \param processId The id of the process, where the Profile was collected.
* \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(
ProcessId processId,
ThreadId threadId,
RuntimeProfileId profileId,
HighResTimeStamp profileStartTimestamp) 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,
HighResDuration 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,
RuntimeProfileId 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 The (root) node. Will be the parent node of the
* corresponding profile tree branch.
* \param idleNodeId Id of 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(
std::vector<RuntimeSamplingProfile::SampleCallStackFrame>&& callStack,
ProfileChunk& chunk,
ProfileTreeNode& rootNode,
uint32_t idleNodeId,
HighResDuration samplesTimeDelta,
NodeIdGenerator& nodeIdGenerator);
/**
* Sends buffered Trace Events via notificationCallback_ and then clears it.
*/
void sendBufferedTraceEventsAndClear();
const std::function<void(folly::dynamic&& traceEventsChunk)>
notificationCallback_;
const uint16_t traceEventChunkSize_;
const uint16_t profileChunkSize_;
folly::dynamic traceEventBuffer_;
static void serializeAndDispatch(
std::vector<RuntimeSamplingProfile>&& profiles,
IdGenerator& profileIdGenerator,
HighResTimeStamp tracingStartTime,
const std::function<void(folly::dynamic&& traceEventsChunk)>&
dispatchCallback,
uint16_t traceEventChunkSize,
uint16_t profileChunkSize = 10);
};
} // namespace facebook::react::jsinspector_modern::tracing
@@ -67,14 +67,18 @@ class RuntimeSamplingProfileTraceEventSerializerTest : public ::testing::Test {
TEST_F(RuntimeSamplingProfileTraceEventSerializerTest, EmptyProfile) {
// Setup
auto notificationCallback = createNotificationCallback();
RuntimeSamplingProfileTraceEventSerializer serializer(
notificationCallback, 10);
IdGenerator profileIdGenerator;
auto profile = createEmptyProfile();
auto tracingStartTime = HighResTimeStamp::now();
// Execute
serializer.serializeAndNotify(std::move(profile), tracingStartTime);
RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
std::move(profile),
profileIdGenerator,
tracingStartTime,
notificationCallback,
10);
// Nothing should be reported if the profile is empty.
EXPECT_TRUE(notificationEvents_.empty());
@@ -85,8 +89,7 @@ TEST_F(
SameCallFramesAreMerged) {
// Setup
auto notificationCallback = createNotificationCallback();
RuntimeSamplingProfileTraceEventSerializer serializer(
notificationCallback, 10);
IdGenerator profileIdGenerator;
// [ foo ]
// [ bar ]
@@ -122,7 +125,12 @@ TEST_F(
auto tracingStartTime = HighResTimeStamp::now();
// Execute
serializer.serializeAndNotify(std::move(profile), tracingStartTime);
RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
std::move(profile),
profileIdGenerator,
tracingStartTime,
notificationCallback,
10);
// Verify
ASSERT_EQ(notificationEvents_.size(), 2);
@@ -135,8 +143,7 @@ TEST_F(
TEST_F(RuntimeSamplingProfileTraceEventSerializerTest, EmptySample) {
// Setup
auto notificationCallback = createNotificationCallback();
RuntimeSamplingProfileTraceEventSerializer serializer(
notificationCallback, 10);
IdGenerator profileIdGenerator;
// Create an empty sample (no call stack)
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> emptyCallStack;
@@ -150,12 +157,13 @@ TEST_F(RuntimeSamplingProfileTraceEventSerializerTest, EmptySample) {
auto tracingStartTime = HighResTimeStamp::now();
// Mock the performance tracer methods
folly::dynamic profileEvent = folly::dynamic::object;
folly::dynamic chunkEvent = folly::dynamic::object;
// Execute
serializer.serializeAndNotify(std::move(profile), tracingStartTime);
RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
std::move(profile),
profileIdGenerator,
tracingStartTime,
notificationCallback,
10);
// Verify
// [["Profile"], ["ProfileChunk"]]
@@ -171,8 +179,7 @@ TEST_F(
SamplesFromDifferentThreads) {
// Setup
auto notificationCallback = createNotificationCallback();
RuntimeSamplingProfileTraceEventSerializer serializer(
notificationCallback, 10);
IdGenerator profileIdGenerator;
// Create samples with different thread IDs
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack = {
@@ -192,7 +199,12 @@ TEST_F(
auto tracingStartTime = HighResTimeStamp::now();
// Execute
serializer.serializeAndNotify(std::move(profile), tracingStartTime);
RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
std::move(profile),
profileIdGenerator,
tracingStartTime,
notificationCallback,
10);
// [["Profile"], ["ProfileChunk", "ProfileChunk", "ProfileChunk]]
// Samples from different thread should never be grouped together in the same
@@ -206,10 +218,9 @@ TEST_F(
TraceEventChunkSizeLimit) {
// Setup
auto notificationCallback = createNotificationCallback();
IdGenerator profileIdGenerator;
uint16_t traceEventChunkSize = 2;
uint16_t profileChunkSize = 2;
RuntimeSamplingProfileTraceEventSerializer serializer(
notificationCallback, traceEventChunkSize, profileChunkSize);
// Create multiple samples
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack = {
@@ -228,7 +239,13 @@ TEST_F(
auto tracingStartTime = HighResTimeStamp::now();
// Execute
serializer.serializeAndNotify(std::move(profile), tracingStartTime);
RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
std::move(profile),
profileIdGenerator,
tracingStartTime,
notificationCallback,
traceEventChunkSize,
profileChunkSize);
// [["Profile"], ["ProfileChunk", "ProfileChunk"], ["ProfileChunk"]]
ASSERT_EQ(notificationEvents_.size(), 3);
@@ -242,12 +259,11 @@ TEST_F(
TEST_F(RuntimeSamplingProfileTraceEventSerializerTest, ProfileChunkSizeLimit) {
// Setup
auto notificationCallback = createNotificationCallback();
IdGenerator profileIdGenerator;
// Set a small profile chunk size to test profile chunking
uint16_t traceEventChunkSize = 10;
uint16_t profileChunkSize = 2;
double samplesCount = 5;
RuntimeSamplingProfileTraceEventSerializer serializer(
notificationCallback, traceEventChunkSize, profileChunkSize);
// Create multiple samples
std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack = {
@@ -266,7 +282,13 @@ TEST_F(RuntimeSamplingProfileTraceEventSerializerTest, ProfileChunkSizeLimit) {
auto tracingStartTime = HighResTimeStamp::now();
// Execute
serializer.serializeAndNotify(std::move(profile), tracingStartTime);
RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch(
std::move(profile),
profileIdGenerator,
tracingStartTime,
notificationCallback,
traceEventChunkSize,
profileChunkSize);
// [["Profile"], ["ProfileChunk", "ProfileChunk", "ProfileChunk"]]
ASSERT_EQ(notificationEvents_.size(), 2);