From 8eea1660f41a45ba7f357645eeab489e6c6003ba Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Mon, 1 Sep 2025 07:12:55 -0700 Subject: [PATCH] Add support for recording Runtime Profiles for multiple JavaScript threads (#53535) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/53535 # Changelog: [Internal] This primarily addressed the case when we have captured a Runtime Profile during the app startup. The Hermes Runtime is created on the main thread, so the first few samples will be recorded there, but then it will be moved to JavaScript thread. Reviewed By: huntie Differential Revision: D81339676 fbshipit-source-id: 8202ca03df54134330aa921a9a0a97816c51cea5 --- ...imeSamplingProfileTraceEventSerializer.cpp | 144 ++++++++++++------ ...amplingProfileTraceEventSerializerTest.cpp | 17 ++- 2 files changed, 106 insertions(+), 55 deletions(-) diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp index fc4730a63a4..466d4a6e1e2 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp @@ -11,6 +11,7 @@ #include "TraceEventSerializer.h" #include +#include namespace facebook::react::jsinspector_modern::tracing { @@ -227,6 +228,51 @@ void sendBufferedTraceEvents( dispatchCallback(std::move(traceEventBuffer)); } +// Auxilliary struct that represents the state of the Profile for a single +// thread. We record a single Profile for a single Thread. +struct ThreadProfileState { + // The current chunk that is being built for this thread. + ProfileChunk chunk; + + // The id of the Profile that is being built for this thread. + RuntimeProfileId profileId; + + ProfileTreeRootNode rootNode; + ProfileTreeNode programNode; + ProfileTreeNode idleNode; + + // The timestamp of the last sample that was captured on this thread. + HighResTimeStamp lastCapturedSampleTimestamp; + + // IdGenerator for this Profile. + IdGenerator nodeIdGenerator; + + ThreadProfileState( + ProcessId processId, + ThreadId threadId, + RuntimeProfileId profileId, + HighResTimeStamp profileTimestamp, + uint16_t chunkSize, + IdGenerator nodeIdGenerator) + : chunk(chunkSize, processId, threadId, profileTimestamp), + profileId(profileId), + rootNode(ProfileTreeRootNode{nodeIdGenerator.getNext()}), + programNode(*rootNode.addChild( + nodeIdGenerator.getNext(), + ProfileTreeNode::CodeType::Other, + createArtificialCallFrame(PROGRAM_FRAME_NAME))), + idleNode(*rootNode.addChild( + nodeIdGenerator.getNext(), + ProfileTreeNode::CodeType::Other, + createArtificialCallFrame(IDLE_FRAME_NAME))), + lastCapturedSampleTimestamp(profileTimestamp), + nodeIdGenerator(nodeIdGenerator) { + chunk.nodes.push_back(rootNode); + chunk.nodes.push_back(programNode); + chunk.nodes.push_back(idleNode); + } +}; + } // namespace /* static */ void @@ -266,56 +312,49 @@ RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch( 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, - 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, threadId, currentChunkTimestamp}; - - IdGenerator nodeIdGenerator{}; - ProfileTreeRootNode rootNode(nodeIdGenerator.getNext()); - chunk.nodes.push_back(rootNode); - - ProfileTreeNode* programNode = rootNode.addChild( - nodeIdGenerator.getNext(), - ProfileTreeNode::CodeType::Other, - createArtificialCallFrame(PROGRAM_FRAME_NAME)); - chunk.nodes.push_back(*programNode); - - ProfileTreeNode* idleNode = rootNode.addChild( - nodeIdGenerator.getNext(), - ProfileTreeNode::CodeType::Other, - createArtificialCallFrame(IDLE_FRAME_NAME)); - chunk.nodes.push_back(*idleNode); - uint32_t idleNodeId = idleNode->getId(); - + std::unordered_map threadProfiles; for (auto& sample : samples) { ThreadId currentSampleThreadId = sample.threadId; auto currentSampleTimestamp = getHighResTimeStampForSample(sample); - // 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()) { + auto threadProfileStateIterator = + threadProfiles.find(currentSampleThreadId); + if (threadProfileStateIterator == threadProfiles.end()) { + RuntimeProfileId nextProfileId = profileIdGenerator.getNext(); + auto profileStartTime = + threadProfiles.empty() ? tracingStartTime : currentSampleTimestamp; + + sendProfileTraceEvent( + profile.processId, + currentSampleThreadId, + nextProfileId, + profileStartTime, + dispatchCallback); + + auto [emplacedThreadProfileStateIterator, _] = threadProfiles.emplace( + currentSampleThreadId, + ThreadProfileState{ + profile.processId, + currentSampleThreadId, + nextProfileId, + profileStartTime, + profileChunkSize, + IdGenerator{}}); + threadProfileStateIterator = emplacedThreadProfileStateIterator; + } + auto& threadProfileState = threadProfileStateIterator->second; + + if (threadProfileState.chunk.isFull()) { bufferProfileChunkTraceEvent( - std::move(chunk), profileId, traceEventBuffer); - chunk = ProfileChunk{ + std::move(threadProfileState.chunk), + threadProfileState.profileId, + traceEventBuffer); + + threadProfileState.chunk = ProfileChunk{ profileChunkSize, profile.processId, currentSampleThreadId, - currentChunkTimestamp}; + tracingStartTime}; } if (traceEventBuffer.size() == traceEventChunkSize) { @@ -327,17 +366,22 @@ RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch( processCallStack( std::move(sample.callStack), - chunk, - rootNode, - idleNodeId, - currentSampleTimestamp - previousSampleTimestamp, - nodeIdGenerator); + threadProfileState.chunk, + threadProfileState.rootNode, + threadProfileState.idleNode.getId(), + currentSampleTimestamp - threadProfileState.lastCapturedSampleTimestamp, + threadProfileState.nodeIdGenerator); - previousSampleTimestamp = currentSampleTimestamp; + threadProfileState.lastCapturedSampleTimestamp = currentSampleTimestamp; } - if (!chunk.isEmpty()) { - bufferProfileChunkTraceEvent(std::move(chunk), profileId, traceEventBuffer); + for (auto& [threadId, threadState] : threadProfiles) { + if (!threadState.chunk.isEmpty()) { + bufferProfileChunkTraceEvent( + std::move(threadState.chunk), + threadState.profileId, + traceEventBuffer); + } } if (!traceEventBuffer.empty()) { 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 95162f03693..dae41580a1c 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/RuntimeSamplingProfileTraceEventSerializerTest.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/RuntimeSamplingProfileTraceEventSerializerTest.cpp @@ -206,11 +206,18 @@ TEST_F( notificationCallback, 10); - // [["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); + /** + * [ + * ["Profile"], + * ["Profile"], + * ["ProfileChunk" for threadId1, "ProfileChunk" for threadId2] + * ] + * + * Samples from different thread should never be grouped together in the same + * chunk. + **/ + ASSERT_EQ(notificationEvents_.size(), 3); + ASSERT_EQ(notificationEvents_[2].size(), 2); } TEST_F(