/* * 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 "PerformanceTracer.h" #include "ProfileTreeNode.h" #include "TraceEventSerializer.h" #include #include namespace facebook::react::jsinspector_modern::tracing { namespace { // To capture samples timestamps Hermes is using steady_clock and returns // them in microseconds granularity since epoch. In the future we might want to // update Hermes to return timestamps in chrono type. HighResTimeStamp getHighResTimeStampForSample( const RuntimeSamplingProfile::Sample& sample) { auto microsecondsSinceSteadyClockEpoch = sample.timestamp; auto chronoTimePoint = std::chrono::steady_clock::time_point( std::chrono::microseconds(microsecondsSinceSteadyClockEpoch)); return HighResTimeStamp::fromChronoSteadyClockTimePoint(chronoTimePoint); } /// 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. constexpr uint32_t FALLBACK_SCRIPT_ID = 0; constexpr std::string_view GARBAGE_COLLECTOR_FRAME_NAME = "(garbage collector)"; constexpr std::string_view ROOT_FRAME_NAME = "(root)"; constexpr std::string_view IDLE_FRAME_NAME = "(idle)"; constexpr std::string_view PROGRAM_FRAME_NAME = "(program)"; TraceEventProfileChunk::CPUProfile::Node convertToTraceEventProfileNode( const ProfileTreeNode& node) { const RuntimeSamplingProfile::SampleCallStackFrame& callFrame = node.getCallFrame(); auto traceEventCallFrame = TraceEventProfileChunk::CPUProfile::Node::CallFrame{ .codeType = node.getCodeType() == ProfileTreeNode::CodeType::JavaScript ? "JS" : "other", .scriptId = callFrame.scriptId, .functionName = std::string(callFrame.functionName), .url = callFrame.scriptURL ? std::optional(std::string(*callFrame.scriptURL)) : std::nullopt, .lineNumber = callFrame.lineNumber, .columnNumber = callFrame.columnNumber, }; return TraceEventProfileChunk::CPUProfile::Node{ .id = node.getId(), .callFrame = std::move(traceEventCallFrame), .parentId = node.hasParent() ? std::optional(node.getParentId()) : std::nullopt, }; } RuntimeSamplingProfile::SampleCallStackFrame createArtificialCallFrame( std::string_view callFrameName) { return RuntimeSamplingProfile::SampleCallStackFrame{ .kind = RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction, .scriptId = FALLBACK_SCRIPT_ID, .functionName = callFrameName, }; }; RuntimeSamplingProfile::SampleCallStackFrame createGarbageCollectorCallFrame() { return RuntimeSamplingProfile::SampleCallStackFrame{ .kind = RuntimeSamplingProfile::SampleCallStackFrame::Kind::GarbageCollector, .scriptId = FALLBACK_SCRIPT_ID, .functionName = GARBAGE_COLLECTOR_FRAME_NAME, }; }; class ProfileTreeRootNode : public ProfileTreeNode { public: explicit ProfileTreeRootNode(uint32_t id) : ProfileTreeNode( id, CodeType::Other, createArtificialCallFrame(ROOT_FRAME_NAME)) {} }; 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); } 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 std::function& dispatchCallback) { auto traceEvent = PerformanceTracer::constructRuntimeProfileTraceEvent( profileId, processId, threadId, profileStartTimestamp); folly::dynamic serializedTraceEvent = TraceEventSerializer::serialize(std::move(traceEvent)); dispatchCallback(folly::dynamic::array(std::move(serializedTraceEvent))); } // Add an empty sample to the chunk. void chunkEmptySample( ProfileChunk& chunk, uint32_t idleNodeId, HighResDuration samplesTimeDelta) { chunk.samples.push_back(idleNodeId); chunk.timeDeltas.push_back(samplesTimeDelta); } // Take the current local ProfileChunk, serialize it as "ProfileChunk" Trace // Event and buffer it. void bufferProfileChunkTraceEvent( ProfileChunk&& chunk, RuntimeProfileId profileId, folly::dynamic& traceEventBuffer) { std::vector traceEventNodes; traceEventNodes.reserve(chunk.nodes.size()); for (const auto& node : chunk.nodes) { traceEventNodes.push_back(convertToTraceEventProfileNode(node)); } auto traceEvent = PerformanceTracer::constructRuntimeProfileChunkTraceEvent( profileId, chunk.processId, chunk.threadId, chunk.timestamp, TraceEventProfileChunk{ .cpuProfile = TraceEventProfileChunk::CPUProfile{ .nodes = std::move(traceEventNodes), .samples = std::move(chunk.samples)}, .timeDeltas = std::move(chunk.timeDeltas), }); auto serializedTraceEvent = TraceEventSerializer::serialize(std::move(traceEvent)); traceEventBuffer.push_back(std::move(serializedTraceEvent)); } // 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, IdGenerator& nodeIdGenerator) { if (callStack.empty()) { chunkEmptySample(chunk, idleNodeId, samplesTimeDelta); return; } ProfileTreeNode* previousNode = &rootNode; for (auto it = callStack.rbegin(); it != callStack.rend(); ++it) { const RuntimeSamplingProfile::SampleCallStackFrame& callFrame = *it; bool isGarbageCollectorFrame = callFrame.kind == RuntimeSamplingProfile::SampleCallStackFrame::Kind::GarbageCollector; ProfileTreeNode::CodeType childCodeType = isGarbageCollectorFrame ? ProfileTreeNode::CodeType::Other : ProfileTreeNode::CodeType::JavaScript; // We don't need real garbage collector call frame, we change it to // what Chrome DevTools expects. RuntimeSamplingProfile::SampleCallStackFrame childCallFrame = isGarbageCollectorFrame ? createGarbageCollectorCallFrame() : callFrame; ProfileTreeNode* maybeExistingChild = previousNode->getIfAlreadyExists(childCodeType, childCallFrame); if (maybeExistingChild != nullptr) { previousNode = maybeExistingChild; } else { previousNode = previousNode->addChild( nodeIdGenerator.getNext(), childCodeType, childCallFrame); chunk.nodes.push_back(*previousNode); } } chunk.samples.push_back(previousNode->getId()); chunk.timeDeltas.push_back(samplesTimeDelta); } // Send buffered Trace Events and reset the buffer. void sendBufferedTraceEvents( folly::dynamic&& traceEventBuffer, const std::function& dispatchCallback) { 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 RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch( std::vector&& profiles, IdGenerator& profileIdGenerator, HighResTimeStamp tracingStartTime, const std::function& dispatchCallback, uint16_t traceEventChunkSize, uint16_t profileChunkSize, uint16_t maxUniqueNodesPerChunk) { for (auto&& profile : profiles) { serializeAndDispatch( std::move(profile), profileIdGenerator, tracingStartTime, dispatchCallback, traceEventChunkSize, profileChunkSize, maxUniqueNodesPerChunk); } } /* static */ void RuntimeSamplingProfileTraceEventSerializer::serializeAndDispatch( RuntimeSamplingProfile&& profile, IdGenerator& profileIdGenerator, HighResTimeStamp tracingStartTime, const std::function& dispatchCallback, uint16_t traceEventChunkSize, uint16_t profileChunkSize, uint16_t maxUniqueNodesPerChunk) { auto samples = std::move(profile.samples); if (samples.empty()) { return; } auto traceEventBuffer = folly::dynamic::array(); traceEventBuffer.reserve(traceEventChunkSize); std::unordered_map threadProfiles; for (auto& sample : samples) { ThreadId currentSampleThreadId = sample.threadId; auto currentSampleTimestamp = getHighResTimeStampForSample(sample); 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() || threadProfileState.chunk.nodes.size() >= maxUniqueNodesPerChunk) { bufferProfileChunkTraceEvent( std::move(threadProfileState.chunk), threadProfileState.profileId, traceEventBuffer); threadProfileState.chunk = ProfileChunk{ profileChunkSize, profile.processId, currentSampleThreadId, tracingStartTime}; } if (traceEventBuffer.size() == traceEventChunkSize) { sendBufferedTraceEvents(std::move(traceEventBuffer), dispatchCallback); traceEventBuffer = folly::dynamic::array(); traceEventBuffer.reserve(traceEventChunkSize); } processCallStack( std::move(sample.callStack), threadProfileState.chunk, threadProfileState.rootNode, threadProfileState.idleNode.getId(), currentSampleTimestamp - threadProfileState.lastCapturedSampleTimestamp, threadProfileState.nodeIdGenerator); threadProfileState.lastCapturedSampleTimestamp = currentSampleTimestamp; } for (auto& [threadId, threadState] : threadProfiles) { if (!threadState.chunk.isEmpty()) { bufferProfileChunkTraceEvent( std::move(threadState.chunk), threadState.profileId, traceEventBuffer); } } if (!traceEventBuffer.empty()) { sendBufferedTraceEvents(std::move(traceEventBuffer), dispatchCallback); } } } // namespace facebook::react::jsinspector_modern::tracing