mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Implement sampling profile serializer (#49191)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49191 # Changelog: [Internal] In this diff we are adding another serializer, that will receive local sampling profile (in tracing domain), and will record corresponding Trace Events with `PerformanceTracer`. It encapsulates the logic of transforming list of samples to `"Profile"` and `"ProfileChunk"` trace events, which will be parsed by Chrome DevTools later. Reviewed By: huntie Differential Revision: D68439735 fbshipit-source-id: 0b3f2b3aff5b79a921e0350759e93f5b05e34d8e
This commit is contained in:
committed by
Facebook GitHub Bot
parent
290f237cfa
commit
87d4300f14
@@ -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<ProfileTreeNode*> children_{};
|
||||
/**
|
||||
* Information about the corresponding call frame that is represented by this
|
||||
* node.
|
||||
*/
|
||||
RuntimeSamplingProfile::SampleCallStackFrame callFrame_;
|
||||
};
|
||||
|
||||
} // namespace facebook::react::jsinspector_modern::tracing
|
||||
@@ -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_;
|
||||
|
||||
+231
@@ -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<std::chrono::microseconds>(
|
||||
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<std::string>(callFrame.getUrl())
|
||||
: std::nullopt,
|
||||
callFrame.hasLineNumber()
|
||||
? std::optional<uint32_t>(callFrame.getLineNumber())
|
||||
: std::nullopt,
|
||||
callFrame.hasColumnNumber()
|
||||
? std::optional<uint32_t>(callFrame.getColumnNumber())
|
||||
: std::nullopt};
|
||||
|
||||
return TraceEventProfileChunk::CPUProfile::Node{
|
||||
node->getId(),
|
||||
traceEventCallFrame,
|
||||
nodeParent != nullptr ? std::optional<uint32_t>(nodeParent->getId())
|
||||
: std::nullopt};
|
||||
}
|
||||
|
||||
void emitSingleProfileChunk(
|
||||
PerformanceTracer& performanceTracer,
|
||||
uint16_t profileId,
|
||||
uint64_t threadId,
|
||||
uint64_t chunkTimestamp,
|
||||
std::vector<ProfileTreeNode*>& nodes,
|
||||
std::vector<uint32_t>& samples,
|
||||
std::vector<long long>& timeDeltas) {
|
||||
std::vector<TraceEventProfileChunk::CPUProfile::Node> 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<RuntimeSamplingProfile::Sample> 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<ProfileTreeNode*> nodesInThisChunk;
|
||||
nodesInThisChunk.push_back(rootNode);
|
||||
nodesInThisChunk.push_back(programNode);
|
||||
nodesInThisChunk.push_back(idleNode);
|
||||
|
||||
std::vector<uint32_t> samplesInThisChunk;
|
||||
samplesInThisChunk.reserve(profileChunkSize);
|
||||
std::vector<long long> 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<RuntimeSamplingProfile::SampleCallStackFrame> 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
|
||||
+30
@@ -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
|
||||
+58
@@ -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 <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user