diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/ProfileTreeNode.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/ProfileTreeNode.h new file mode 100644 index 00000000000..36b189f2f35 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/ProfileTreeNode.h @@ -0,0 +1,111 @@ +/* + * 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. + */ + +#pragma once + +#include "RuntimeSamplingProfile.h" + +namespace facebook::react::jsinspector_modern::tracing { + +/* + * Auxiliary data structure used for creating Profile tree and identifying + * identical frames. + */ +class ProfileTreeNode { + public: + /* + * For Chromium & V8 this could also be WASM, this is not the case for us. + */ + enum class CodeType { + JavaScript, + Other, + }; + + ProfileTreeNode( + uint32_t id, + CodeType codeType, + ProfileTreeNode* parent, + RuntimeSamplingProfile::SampleCallStackFrame callFrame) + : id_(id), + codeType_(codeType), + parent_(parent), + callFrame_(std::move(callFrame)) {} + + uint32_t getId() const { + return id_; + } + + CodeType getCodeType() const { + return codeType_; + } + + /** + * \return pointer to the parent node, nullptr if this is the root node. + */ + ProfileTreeNode* getParent() const { + return parent_; + } + + /** + * \return call frame information that is represented by this node. + */ + const RuntimeSamplingProfile::SampleCallStackFrame& getCallFrame() const { + return callFrame_; + } + + /** + * Will only add unique child node. Returns 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) { + return existingChild; + } + } + + children_.push_back(child); + return nullptr; + } + + bool operator==(const ProfileTreeNode* rhs) const { + if (this->parent_ != rhs->parent_) { + return false; + } + if (this->codeType_ != rhs->codeType_) { + return false; + } + + return this->getCallFrame() == rhs->getCallFrame(); + } + + private: + /** + * Unique id of the node. + */ + uint32_t id_; + /** + * Type of the code that is represented by this node. Either JavaScript or + * Other. + */ + CodeType codeType_; + /** + * Pointer to the parent node. Should be nullptr only for root node. + */ + ProfileTreeNode* parent_{nullptr}; + /** + * Lst of pointers to children nodes. + */ + std::vector children_{}; + /** + * Information about the corresponding call frame that is represented by this + * node. + */ + RuntimeSamplingProfile::SampleCallStackFrame callFrame_; +}; + +} // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfile.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfile.h index f51d3523c0c..aeb86d8948c 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfile.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfile.h @@ -86,6 +86,12 @@ struct RuntimeSamplingProfile { return columnNumber_.value(); } + inline bool operator==(const SampleCallStackFrame& rhs) const noexcept { + return kind_ == rhs.kind_ && scriptId_ == rhs.scriptId_ && + functionName_ == rhs.functionName_ && url_ == rhs.url_ && + lineNumber_ == rhs.lineNumber_ && columnNumber_ == rhs.columnNumber_; + } + private: Kind kind_; uint32_t scriptId_; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp b/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp new file mode 100644 index 00000000000..61f85386c4f --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.cpp @@ -0,0 +1,231 @@ +/* + * 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 "RuntimeSamplingProfileTraceEventSerializer.h" +#include "ProfileTreeNode.h" + +namespace facebook::react::jsinspector_modern::tracing { + +namespace { + +uint64_t formatTimePointToUnixTimestamp( + std::chrono::steady_clock::time_point timestamp) { + return std::chrono::duration_cast( + timestamp.time_since_epoch()) + .count(); +} + +TraceEventProfileChunk::CPUProfile::Node convertToTraceEventProfileNode( + ProfileTreeNode* node) { + ProfileTreeNode* nodeParent = node->getParent(); + const RuntimeSamplingProfile::SampleCallStackFrame& callFrame = + node->getCallFrame(); + auto traceEventCallFrame = + TraceEventProfileChunk::CPUProfile::Node::CallFrame{ + node->getCodeType() == ProfileTreeNode::CodeType::JavaScript + ? "JS" + : "other", + callFrame.getScriptId(), + callFrame.getFunctionName(), + callFrame.hasUrl() ? std::optional(callFrame.getUrl()) + : std::nullopt, + callFrame.hasLineNumber() + ? std::optional(callFrame.getLineNumber()) + : std::nullopt, + callFrame.hasColumnNumber() + ? std::optional(callFrame.getColumnNumber()) + : std::nullopt}; + + return TraceEventProfileChunk::CPUProfile::Node{ + node->getId(), + traceEventCallFrame, + nodeParent != nullptr ? std::optional(nodeParent->getId()) + : std::nullopt}; +} + +void emitSingleProfileChunk( + PerformanceTracer& performanceTracer, + uint16_t profileId, + uint64_t threadId, + uint64_t chunkTimestamp, + std::vector& nodes, + std::vector& samples, + std::vector& timeDeltas) { + std::vector traceEventNodes; + traceEventNodes.reserve(nodes.size()); + for (ProfileTreeNode* node : nodes) { + traceEventNodes.push_back(convertToTraceEventProfileNode(node)); + } + + performanceTracer.reportRuntimeProfileChunk( + profileId, + threadId, + chunkTimestamp, + TraceEventProfileChunk{ + TraceEventProfileChunk::CPUProfile{traceEventNodes, samples}, + TraceEventProfileChunk::TimeDeltas{timeDeltas}, + }); +} + +} // namespace + +/* static */ void +RuntimeSamplingProfileTraceEventSerializer::serializeAndBuffer( + PerformanceTracer& performanceTracer, + const RuntimeSamplingProfile& profile, + std::chrono::steady_clock::time_point tracingStartTime, + uint16_t profileChunkSize) { + std::vector runtimeSamples = + profile.getSamples(); + if (runtimeSamples.empty()) { + return; + } + + uint64_t chunkThreadId = runtimeSamples.front().getThreadId(); + uint64_t tracingStartUnixTimestamp = + formatTimePointToUnixTimestamp(tracingStartTime); + uint16_t profileId = performanceTracer.reportRuntimeProfile( + chunkThreadId, 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)"}); + + 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 : runtimeSamples) { + 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, + profileId, + chunkThreadId, + chunkTimestamp, + nodesInThisChunk, + samplesInThisChunk, + timeDeltasInThisChunk); + + nodesInThisChunk.clear(); + samplesInThisChunk.clear(); + timeDeltasInThisChunk.clear(); + } + + chunkThreadId = sampleThreadId; + std::vector callStack = + sample.getCallStack(); + uint64_t sampleTimestamp = sample.getTimestamp(); + if (samplesInThisChunk.empty()) { + // New chunk. Reset the timestamp. + chunkTimestamp = sampleTimestamp; + } + + 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, + profileId, + chunkThreadId, + chunkTimestamp, + nodesInThisChunk, + samplesInThisChunk, + timeDeltasInThisChunk); + + nodesInThisChunk.clear(); + samplesInThisChunk.clear(); + timeDeltasInThisChunk.clear(); + } + } + + if (!samplesInThisChunk.empty()) { + emitSingleProfileChunk( + performanceTracer, + profileId, + chunkThreadId, + chunkTimestamp, + nodesInThisChunk, + samplesInThisChunk, + timeDeltasInThisChunk); + } +} + +} // namespace facebook::react::jsinspector_modern::tracing diff --git a/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.h b/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.h new file mode 100644 index 00000000000..271d0a7463a --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/RuntimeSamplingProfileTraceEventSerializer.h @@ -0,0 +1,30 @@ +/* + * 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. + */ + +#pragma once + +#include "PerformanceTracer.h" +#include "RuntimeSamplingProfile.h" + +namespace facebook::react::jsinspector_modern::tracing { + +/** + * Serializes RuntimeSamplingProfile into collection of specific Trace Events, + * which represent Profile information on a timeline. + */ +class RuntimeSamplingProfileTraceEventSerializer { + public: + RuntimeSamplingProfileTraceEventSerializer() = delete; + + static void serializeAndBuffer( + PerformanceTracer& performanceTracer, + const RuntimeSamplingProfile& profile, + std::chrono::steady_clock::time_point tracingStartTime, + uint16_t profileChunkSize = 100); +}; + +} // 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 new file mode 100644 index 00000000000..d06896ff15f --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/tracing/tests/ProfileTreeNodeTest.cpp @@ -0,0 +1,58 @@ +/* + * 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 "ProfileTreeNode.h" + +#include +#include + +namespace facebook::react::jsinspector_modern::tracing { + +TEST(ProfileTreeNodeTest, EqualityOperator) { + auto callFrame = RuntimeSamplingProfile::SampleCallStackFrame{ + RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction, 0, "foo"}; + ProfileTreeNode* node1; + ProfileTreeNode* node2; + + node1 = new ProfileTreeNode( + 1, ProfileTreeNode::CodeType::JavaScript, nullptr, callFrame); + node2 = new ProfileTreeNode( + 2, ProfileTreeNode::CodeType::JavaScript, nullptr, callFrame); + EXPECT_EQ(*node1 == node2, true); + + node1 = new ProfileTreeNode( + 3, ProfileTreeNode::CodeType::JavaScript, 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); + node2 = new ProfileTreeNode( + 6, ProfileTreeNode::CodeType::JavaScript, node2, callFrame); + EXPECT_EQ(*node1 == node2, true); +} + +TEST(ProfileTreeNodeTest, OnlyAddsUniqueChildren) { + auto callFrame = RuntimeSamplingProfile::SampleCallStackFrame{ + RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction, 0, "foo"}; + ProfileTreeNode* parent = new ProfileTreeNode( + 1, ProfileTreeNode::CodeType::JavaScript, nullptr, callFrame); + ProfileTreeNode* existingChild = new ProfileTreeNode( + 2, ProfileTreeNode::CodeType::JavaScript, parent, callFrame); + + auto maybeAlreadyExistingChild = parent->addChild(existingChild); + EXPECT_EQ(maybeAlreadyExistingChild, nullptr); + + auto copyOfExistingChild = new ProfileTreeNode( + 3, ProfileTreeNode::CodeType::JavaScript, parent, callFrame); + + maybeAlreadyExistingChild = parent->addChild(copyOfExistingChild); + EXPECT_EQ(maybeAlreadyExistingChild, existingChild); +} + +} // namespace facebook::react::jsinspector_modern::tracing