diff --git a/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp index 80bade26692..24bcb0d25e1 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp @@ -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", diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp index cff8e412dbd..fc4730a63a4 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp @@ -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(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 nodes; + std::vector samples; + std::vector 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& + 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 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&& 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& + dispatchCallback) { + dispatchCallback(std::move(traceEventBuffer)); } -void RuntimeSamplingProfileTraceEventSerializer::serializeAndNotify( +} // namespace + +/* static */ void +RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch( + std::vector&& profiles, + IdGenerator& profileIdGenerator, + HighResTimeStamp tracingStartTime, + const std::function& + 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& + 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); } } diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.h index 0f26efd5669..05dd20d12b5 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.h @@ -7,16 +7,13 @@ #pragma once -#include "ProfileTreeNode.h" #include "RuntimeSamplingProfile.h" #include 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 nodes; - std::vector samples; - std::vector 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 - 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& + 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&& 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 - notificationCallback_; - const uint16_t traceEventChunkSize_; - const uint16_t profileChunkSize_; - - folly::dynamic traceEventBuffer_; + static void serializeAndDispatch( + std::vector&& profiles, + IdGenerator& profileIdGenerator, + HighResTimeStamp tracingStartTime, + const std::function& + dispatchCallback, + uint16_t traceEventChunkSize, + uint16_t profileChunkSize = 10); }; } // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/RuntimeSamplingProfileTraceEventSerializerTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/RuntimeSamplingProfileTraceEventSerializerTest.cpp index 45489c5cd3b..95162f03693 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/RuntimeSamplingProfileTraceEventSerializerTest.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/RuntimeSamplingProfileTraceEventSerializerTest.cpp @@ -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 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 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 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 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);