|
| 1 | +/* |
| 2 | + * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | + * |
| 4 | + * This source code is licensed under the MIT license found in the |
| 5 | + * LICENSE file in the root directory of this source tree. |
| 6 | + */ |
| 7 | + |
| 8 | +#include "RuntimeSamplingProfileTraceEventSerializer.h" |
| 9 | +#include "ProfileTreeNode.h" |
| 10 | + |
| 11 | +namespace facebook::react::jsinspector_modern::tracing { |
| 12 | + |
| 13 | +namespace { |
| 14 | + |
| 15 | +uint64_t formatTimePointToUnixTimestamp( |
| 16 | + std::chrono::steady_clock::time_point timestamp) { |
| 17 | + return std::chrono::duration_cast<std::chrono::microseconds>( |
| 18 | + timestamp.time_since_epoch()) |
| 19 | + .count(); |
| 20 | +} |
| 21 | + |
| 22 | +TraceEventProfileChunk::CPUProfile::Node convertToTraceEventProfileNode( |
| 23 | + ProfileTreeNode* node) { |
| 24 | + ProfileTreeNode* nodeParent = node->getParent(); |
| 25 | + const RuntimeSamplingProfile::SampleCallStackFrame& callFrame = |
| 26 | + node->getCallFrame(); |
| 27 | + auto traceEventCallFrame = |
| 28 | + TraceEventProfileChunk::CPUProfile::Node::CallFrame{ |
| 29 | + node->getCodeType() == ProfileTreeNode::CodeType::JavaScript |
| 30 | + ? "JS" |
| 31 | + : "other", |
| 32 | + callFrame.getScriptId(), |
| 33 | + callFrame.getFunctionName(), |
| 34 | + callFrame.hasUrl() ? std::optional<std::string>(callFrame.getUrl()) |
| 35 | + : std::nullopt, |
| 36 | + callFrame.hasLineNumber() |
| 37 | + ? std::optional<uint32_t>(callFrame.getLineNumber()) |
| 38 | + : std::nullopt, |
| 39 | + callFrame.hasColumnNumber() |
| 40 | + ? std::optional<uint32_t>(callFrame.getColumnNumber()) |
| 41 | + : std::nullopt}; |
| 42 | + |
| 43 | + return TraceEventProfileChunk::CPUProfile::Node{ |
| 44 | + node->getId(), |
| 45 | + traceEventCallFrame, |
| 46 | + nodeParent != nullptr ? std::optional<uint32_t>(nodeParent->getId()) |
| 47 | + : std::nullopt}; |
| 48 | +} |
| 49 | + |
| 50 | +void emitSingleProfileChunk( |
| 51 | + PerformanceTracer& performanceTracer, |
| 52 | + uint16_t profileId, |
| 53 | + uint64_t threadId, |
| 54 | + uint64_t chunkTimestamp, |
| 55 | + std::vector<ProfileTreeNode*>& nodes, |
| 56 | + std::vector<uint32_t>& samples, |
| 57 | + std::vector<long long>& timeDeltas) { |
| 58 | + std::vector<TraceEventProfileChunk::CPUProfile::Node> traceEventNodes; |
| 59 | + traceEventNodes.reserve(nodes.size()); |
| 60 | + for (ProfileTreeNode* node : nodes) { |
| 61 | + traceEventNodes.push_back(convertToTraceEventProfileNode(node)); |
| 62 | + } |
| 63 | + |
| 64 | + performanceTracer.reportRuntimeProfileChunk( |
| 65 | + profileId, |
| 66 | + threadId, |
| 67 | + chunkTimestamp, |
| 68 | + TraceEventProfileChunk{ |
| 69 | + TraceEventProfileChunk::CPUProfile{traceEventNodes, samples}, |
| 70 | + TraceEventProfileChunk::TimeDeltas{timeDeltas}, |
| 71 | + }); |
| 72 | +} |
| 73 | + |
| 74 | +} // namespace |
| 75 | + |
| 76 | +/* static */ void |
| 77 | +RuntimeSamplingProfileTraceEventSerializer::serializeAndBuffer( |
| 78 | + PerformanceTracer& performanceTracer, |
| 79 | + const RuntimeSamplingProfile& profile, |
| 80 | + std::chrono::steady_clock::time_point tracingStartTime, |
| 81 | + uint16_t profileChunkSize) { |
| 82 | + std::vector<RuntimeSamplingProfile::Sample> runtimeSamples = |
| 83 | + profile.getSamples(); |
| 84 | + if (runtimeSamples.empty()) { |
| 85 | + return; |
| 86 | + } |
| 87 | + |
| 88 | + uint64_t chunkThreadId = runtimeSamples.front().getThreadId(); |
| 89 | + uint64_t tracingStartUnixTimestamp = |
| 90 | + formatTimePointToUnixTimestamp(tracingStartTime); |
| 91 | + uint16_t profileId = performanceTracer.reportRuntimeProfile( |
| 92 | + chunkThreadId, tracingStartUnixTimestamp); |
| 93 | + |
| 94 | + uint32_t nodeCount = 0; |
| 95 | + auto* rootNode = new ProfileTreeNode( |
| 96 | + ++nodeCount, |
| 97 | + ProfileTreeNode::CodeType::Other, |
| 98 | + nullptr, |
| 99 | + RuntimeSamplingProfile::SampleCallStackFrame{ |
| 100 | + RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction, |
| 101 | + 0, |
| 102 | + "(root)"}); |
| 103 | + auto* programNode = new ProfileTreeNode( |
| 104 | + ++nodeCount, |
| 105 | + ProfileTreeNode::CodeType::Other, |
| 106 | + rootNode, |
| 107 | + RuntimeSamplingProfile::SampleCallStackFrame{ |
| 108 | + RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction, |
| 109 | + 0, |
| 110 | + "(program)"}); |
| 111 | + auto* idleNode = new ProfileTreeNode( |
| 112 | + ++nodeCount, |
| 113 | + ProfileTreeNode::CodeType::Other, |
| 114 | + rootNode, |
| 115 | + RuntimeSamplingProfile::SampleCallStackFrame{ |
| 116 | + RuntimeSamplingProfile::SampleCallStackFrame::Kind::JSFunction, |
| 117 | + 0, |
| 118 | + "(idle)"}); |
| 119 | + |
| 120 | + rootNode->addChild(programNode); |
| 121 | + rootNode->addChild(idleNode); |
| 122 | + |
| 123 | + // Ideally, we should use a timestamp from Runtime Sampling Profiler. |
| 124 | + // We currently use tracingStartTime, which is defined in TracingAgent. |
| 125 | + uint64_t previousSampleTimestamp = tracingStartUnixTimestamp; |
| 126 | + // There could be any number of new nodes in this chunk. Empty if all nodes |
| 127 | + // are already emitted in previous chunks. |
| 128 | + std::vector<ProfileTreeNode*> nodesInThisChunk; |
| 129 | + nodesInThisChunk.push_back(rootNode); |
| 130 | + nodesInThisChunk.push_back(programNode); |
| 131 | + nodesInThisChunk.push_back(idleNode); |
| 132 | + |
| 133 | + std::vector<uint32_t> samplesInThisChunk; |
| 134 | + samplesInThisChunk.reserve(profileChunkSize); |
| 135 | + std::vector<long long> timeDeltasInThisChunk; |
| 136 | + timeDeltasInThisChunk.reserve(profileChunkSize); |
| 137 | + |
| 138 | + RuntimeSamplingProfile::SampleCallStackFrame garbageCollectorCallFrame = |
| 139 | + RuntimeSamplingProfile::SampleCallStackFrame{ |
| 140 | + RuntimeSamplingProfile::SampleCallStackFrame::Kind::GarbageCollector, |
| 141 | + 0, |
| 142 | + "(garbage collector)"}; |
| 143 | + uint64_t chunkTimestamp = tracingStartUnixTimestamp; |
| 144 | + for (const RuntimeSamplingProfile::Sample& sample : runtimeSamples) { |
| 145 | + uint64_t sampleThreadId = sample.getThreadId(); |
| 146 | + // If next sample was recorded on a different thread, emit the current chunk |
| 147 | + // and continue. |
| 148 | + if (chunkThreadId != sampleThreadId) { |
| 149 | + emitSingleProfileChunk( |
| 150 | + performanceTracer, |
| 151 | + profileId, |
| 152 | + chunkThreadId, |
| 153 | + chunkTimestamp, |
| 154 | + nodesInThisChunk, |
| 155 | + samplesInThisChunk, |
| 156 | + timeDeltasInThisChunk); |
| 157 | + |
| 158 | + nodesInThisChunk.clear(); |
| 159 | + samplesInThisChunk.clear(); |
| 160 | + timeDeltasInThisChunk.clear(); |
| 161 | + } |
| 162 | + |
| 163 | + chunkThreadId = sampleThreadId; |
| 164 | + std::vector<RuntimeSamplingProfile::SampleCallStackFrame> callStack = |
| 165 | + sample.getCallStack(); |
| 166 | + uint64_t sampleTimestamp = sample.getTimestamp(); |
| 167 | + if (samplesInThisChunk.empty()) { |
| 168 | + // New chunk. Reset the timestamp. |
| 169 | + chunkTimestamp = sampleTimestamp; |
| 170 | + } |
| 171 | + |
| 172 | + long long timeDelta = sampleTimestamp - previousSampleTimestamp; |
| 173 | + timeDeltasInThisChunk.push_back(timeDelta); |
| 174 | + previousSampleTimestamp = sampleTimestamp; |
| 175 | + |
| 176 | + ProfileTreeNode* previousNode = callStack.empty() ? idleNode : rootNode; |
| 177 | + for (auto it = callStack.rbegin(); it != callStack.rend(); ++it) { |
| 178 | + auto callFrame = *it; |
| 179 | + bool isGarbageCollectorFrame = callFrame.getKind() == |
| 180 | + RuntimeSamplingProfile::SampleCallStackFrame::Kind::GarbageCollector; |
| 181 | + // We don't need real garbage collector call frame, we change it to |
| 182 | + // what Chrome DevTools expects. |
| 183 | + auto* currentNode = new ProfileTreeNode( |
| 184 | + nodeCount + 1, |
| 185 | + isGarbageCollectorFrame ? ProfileTreeNode::CodeType::Other |
| 186 | + : ProfileTreeNode::CodeType::JavaScript, |
| 187 | + previousNode, |
| 188 | + isGarbageCollectorFrame ? garbageCollectorCallFrame : callFrame); |
| 189 | + |
| 190 | + ProfileTreeNode* alreadyExistingNode = |
| 191 | + previousNode->addChild(currentNode); |
| 192 | + if (alreadyExistingNode != nullptr) { |
| 193 | + previousNode = alreadyExistingNode; |
| 194 | + } else { |
| 195 | + nodesInThisChunk.push_back(currentNode); |
| 196 | + ++nodeCount; |
| 197 | + |
| 198 | + previousNode = currentNode; |
| 199 | + } |
| 200 | + } |
| 201 | + samplesInThisChunk.push_back(previousNode->getId()); |
| 202 | + |
| 203 | + if (samplesInThisChunk.size() == profileChunkSize) { |
| 204 | + emitSingleProfileChunk( |
| 205 | + performanceTracer, |
| 206 | + profileId, |
| 207 | + chunkThreadId, |
| 208 | + chunkTimestamp, |
| 209 | + nodesInThisChunk, |
| 210 | + samplesInThisChunk, |
| 211 | + timeDeltasInThisChunk); |
| 212 | + |
| 213 | + nodesInThisChunk.clear(); |
| 214 | + samplesInThisChunk.clear(); |
| 215 | + timeDeltasInThisChunk.clear(); |
| 216 | + } |
| 217 | + } |
| 218 | + |
| 219 | + if (!samplesInThisChunk.empty()) { |
| 220 | + emitSingleProfileChunk( |
| 221 | + performanceTracer, |
| 222 | + profileId, |
| 223 | + chunkThreadId, |
| 224 | + chunkTimestamp, |
| 225 | + nodesInThisChunk, |
| 226 | + samplesInThisChunk, |
| 227 | + timeDeltasInThisChunk); |
| 228 | + } |
| 229 | +} |
| 230 | + |
| 231 | +} // namespace facebook::react::jsinspector_modern::tracing |
0 commit comments