diff --git a/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp index b77751889cd0ee..28407ee865db82 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/TracingAgent.cpp @@ -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", diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/ProfileTreeNode.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/ProfileTreeNode.h index 36b189f2f353df..c72560f31081da 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/ProfileTreeNode.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/ProfileTreeNode.h @@ -7,7 +7,10 @@ #pragma once -#include "RuntimeSamplingProfile.h" +#include +#include + +#include namespace facebook::react::jsinspector_modern::tracing { @@ -28,7 +31,7 @@ class ProfileTreeNode { ProfileTreeNode( uint32_t id, CodeType codeType, - ProfileTreeNode* parent, + std::shared_ptr 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 addChild( + std::shared_ptr 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 parent_; /** - * Lst of pointers to children nodes. + * Lst of shared pointers to children nodes. */ - std::vector children_{}; + std::vector> children_; /** * Information about the corresponding call frame that is represented by this * node. diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp index 573d4c566cbed1..bf2468e0a63c71 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp @@ -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( @@ -24,7 +29,7 @@ uint64_t formatTimePointToUnixTimestamp( } TraceEventProfileChunk::CPUProfile::Node convertToTraceEventProfileNode( - ProfileTreeNode* node) { + std::shared_ptr 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& buffer, - uint16_t profileId, +RuntimeSamplingProfile::SampleCallStackFrame createArtificialCallFrame( + std::string callFrameName) { + return RuntimeSamplingProfile::SampleCallStackFrame{ + RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction, + FALLBACK_SCRIPT_ID, + std::move(callFrameName)}; +}; + +RuntimeSamplingProfile::SampleCallStackFrame createGarbageCollectorCallFrame() { + return RuntimeSamplingProfile::SampleCallStackFrame{ + RuntimeSamplingProfile::SampleCallStackFrame::Kind::GarbageCollector, + FALLBACK_SCRIPT_ID, + "(garbage collector)"}; +}; + +std::shared_ptr createNode( + NodeIdGenerator& nodeIdGenerator, + ProfileTreeNode::CodeType codeType, + std::shared_ptr parent, + const RuntimeSamplingProfile::SampleCallStackFrame& callFrame) { + return std::make_shared( + nodeIdGenerator.getNext(), codeType, parent, callFrame); +} + +std::shared_ptr createArtificialNode( + NodeIdGenerator& nodeIdGenerator, + std::shared_ptr parent, + std::string callFrameName) { + RuntimeSamplingProfile::SampleCallStackFrame callFrame = + createArtificialCallFrame(std::move(callFrameName)); + + return createNode( + nodeIdGenerator, ProfileTreeNode::CodeType::Other, parent, callFrame); +} + +std::shared_ptr createRootNode( + NodeIdGenerator& nodeIdGenerator) { + return createArtificialNode(nodeIdGenerator, nullptr, "(root)"); +} + +std::shared_ptr createProgramNode( + NodeIdGenerator& nodeIdGenerator, + std::shared_ptr rootNode) { + return createArtificialNode(nodeIdGenerator, rootNode, "(program)"); +} + +std::shared_ptr createIdleNode( + NodeIdGenerator& nodeIdGenerator, + std::shared_ptr rootNode) { + return createArtificialNode(nodeIdGenerator, rootNode, "(idle)"); +} + +} // namespace + +void RuntimeSamplingProfileTraceEventSerializer::sendProfileTraceEvent( uint64_t threadId, - uint64_t chunkTimestamp, - std::vector& nodes, - std::vector& samples, - std::vector& timeDeltas) { + 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 traceEventNodes; - traceEventNodes.reserve(nodes.size()); - for (ProfileTreeNode* node : nodes) { + traceEventNodes.reserve(chunk.nodes.size()); + for (const auto& node : chunk.nodes) { traceEventNodes.push_back(convertToTraceEventProfileNode(node)); } - buffer.push_back(performanceTracer.getSerializedRuntimeProfileChunkTraceEvent( - profileId, - threadId, - chunkTimestamp, - TraceEventProfileChunk{ - TraceEventProfileChunk::CPUProfile{traceEventNodes, samples}, - TraceEventProfileChunk::TimeDeltas{timeDeltas}, - })); + traceEventBuffer_.push_back( + performanceTracer_.getSerializedRuntimeProfileChunkTraceEvent( + profileId, + chunk.threadId, + chunk.timestamp, + TraceEventProfileChunk{ + TraceEventProfileChunk::CPUProfile{ + traceEventNodes, chunk.samples}, + TraceEventProfileChunk::TimeDeltas{chunk.timeDeltas}, + })); } -} // namespace +void RuntimeSamplingProfileTraceEventSerializer::processCallStack( + const std::vector& callStack, + ProfileChunk& chunk, + std::shared_ptr rootNode, + std::shared_ptr 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; + } + } -/* static */ void -RuntimeSamplingProfileTraceEventSerializer::serializeAndNotify( - PerformanceTracer& performanceTracer, + 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& - notificationCallback, - uint16_t traceEventChunkSize, - uint16_t profileChunkSize) { + std::chrono::steady_clock::time_point tracingStartTime) { const std::vector& samples = profile.getSamples(); if (samples.empty()) { return; } - std::vector 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)); - - 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)"}); + uint64_t previousSampleUnixTimestamp = tracingStartUnixTimestamp; + uint64_t currentChunkUnixTimestamp = tracingStartUnixTimestamp; + + 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 nodesInThisChunk; - nodesInThisChunk.push_back(rootNode); - nodesInThisChunk.push_back(programNode); - nodesInThisChunk.push_back(idleNode); - - std::vector samplesInThisChunk; - samplesInThisChunk.reserve(profileChunkSize); - std::vector 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 : 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(); - } + ProfileChunk chunk{ + profileChunkSize_, firstChunkThreadId, currentChunkUnixTimestamp}; + chunk.nodes.push_back(rootNode); + chunk.nodes.push_back(programNode); + chunk.nodes.push_back(idleNode); - chunkThreadId = sampleThreadId; - std::vector callStack = - sample.getCallStack(); - uint64_t sampleTimestamp = sample.getTimestamp(); - if (samplesInThisChunk.empty()) { - // New chunk. Reset the timestamp. - chunkTimestamp = sampleTimestamp; - } + for (const auto& sample : samples) { + uint64_t currentSampleThreadId = sample.getThreadId(); + long long currentSampleUnixTimestamp = sample.getTimestamp(); - 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, - 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}; } - if (buffer.size() == traceEventChunkSize) { - notificationCallback(folly::dynamic::array(buffer.begin(), buffer.end())); - buffer.clear(); + if (traceEventBuffer_.size() == traceEventChunkSize_) { + sendBufferedTraceEventsAndClear(); } + + processCallStack( + sample.getCallStack(), + chunk, + rootNode, + idleNode, + currentSampleUnixTimestamp - previousSampleUnixTimestamp, + nodeIdGenerator); + + 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(); } } diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.h index 80eb7d238ed567..81202b43fba5e7 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.h @@ -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> nodes; + std::vector samples; + std::vector 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& + std::function 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& + callStack, + ProfileChunk& chunk, + std::shared_ptr rootNode, + std::shared_ptr idleNode, + long long samplesTimeDelta, + NodeIdGenerator& nodeIdGenerator); + + /** + * Sends buffered Trace Events via notificationCallback_ and then clears it. + */ + void sendBufferedTraceEventsAndClear(); + + PerformanceTracer& performanceTracer_; + const std::function + notificationCallback_; + uint16_t traceEventChunkSize_; + uint16_t profileChunkSize_; + + folly::dynamic traceEventBuffer_; }; } // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/ProfileTreeNodeTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/ProfileTreeNodeTest.cpp index d06896ff15fc55..a49f3b5c254c81 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/ProfileTreeNodeTest.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/ProfileTreeNodeTest.cpp @@ -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(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(node2), + callFrame); node2 = new ProfileTreeNode( - 6, ProfileTreeNode::CodeType::JavaScript, node2, callFrame); + 6, + ProfileTreeNode::CodeType::JavaScript, + std::shared_ptr(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( 1, ProfileTreeNode::CodeType::JavaScript, nullptr, callFrame); - ProfileTreeNode* existingChild = new ProfileTreeNode( + auto existingChild = std::make_shared( 2, ProfileTreeNode::CodeType::JavaScript, parent, callFrame); auto maybeAlreadyExistingChild = parent->addChild(existingChild); EXPECT_EQ(maybeAlreadyExistingChild, nullptr); - auto copyOfExistingChild = new ProfileTreeNode( + auto copyOfExistingChild = std::make_shared( 3, ProfileTreeNode::CodeType::JavaScript, parent, callFrame); maybeAlreadyExistingChild = parent->addChild(copyOfExistingChild); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/RuntimeSamplingProfileTraceEventSerializerTest.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/RuntimeSamplingProfileTraceEventSerializerTest.cpp new file mode 100644 index 00000000000000..7b923f05d06b3e --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/RuntimeSamplingProfileTraceEventSerializerTest.cpp @@ -0,0 +1,291 @@ +/* + * 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 + +#include +#include +#include +#include + +namespace facebook::react::jsinspector_modern::tracing { + +class RuntimeSamplingProfileTraceEventSerializerTest : public ::testing::Test { + protected: + std::vector notificationEvents_; + + std::function + createNotificationCallback() { + return [this](const folly::dynamic& traceEventsChunk) { + notificationEvents_.push_back(traceEventsChunk); + }; + } + + RuntimeSamplingProfile::SampleCallStackFrame createJSCallFrame( + std::string functionName, + uint32_t scriptId = 1, + std::optional url = std::nullopt, + std::optional lineNumber = std::nullopt, + std::optional 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 callStack) { + return RuntimeSamplingProfile::Sample( + timestamp, threadId, std::move(callStack)); + } + + RuntimeSamplingProfile createEmptyProfile() { + return {"TestRuntime", {}}; + } + + RuntimeSamplingProfile createProfileWithSamples( + std::vector 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 callStack1 = { + createJSCallFrame("bar", 1, "test.js", 20, 10), + createJSCallFrame("foo", 1, "test.js", 10, 5), + }; + + std::vector callStack2 = { + createJSCallFrame("baz", 1, "other.js", 5, 1), + createJSCallFrame("bar", 1, "test.js", 20, 10), + createJSCallFrame("foo", 1, "test.js", 10, 5), + }; + + std::vector 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{}; + 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 emptyCallStack; + + uint64_t threadId = 1; + uint64_t timestamp = 1000000; + + auto samples = std::vector{}; + 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 callStack = { + createJSCallFrame("foo", 1, "test.js", 10, 5)}; + + uint64_t timestamp = 1000000; + uint64_t threadId1 = 1; + uint64_t threadId2 = 2; + + auto samples = std::vector{}; + 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 callStack = { + createJSCallFrame("foo", 1, "test.js", 10, 5)}; + + uint64_t timestamp = 1000000; + uint64_t threadId = 1; + + std::vector 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 callStack = { + createJSCallFrame("foo", 1, "test.js", 10, 5)}; + + uint64_t timestamp = 1000000; + uint64_t threadId = 1; + + std::vector 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