/src/hermes/lib/VM/Profiler/SamplingProfiler.cpp
Line | Count | Source (jump to first uncovered line) |
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 "hermes/VM/Profiler/SamplingProfiler.h" |
9 | | |
10 | | #if HERMESVM_SAMPLING_PROFILER_AVAILABLE |
11 | | |
12 | | #include "hermes/VM/Callable.h" |
13 | | #include "hermes/VM/HostModel.h" |
14 | | #include "hermes/VM/Runtime.h" |
15 | | #include "hermes/VM/RuntimeModule-inline.h" |
16 | | #include "hermes/VM/StackFrame-inline.h" |
17 | | |
18 | | #include "llvh/Support/Compiler.h" |
19 | | |
20 | | #include "ChromeTraceSerializer.h" |
21 | | #include "ProfileGenerator.h" |
22 | | #include "SamplingProfilerSampler.h" |
23 | | |
24 | | #include <fcntl.h> |
25 | | #include <cassert> |
26 | | #include <chrono> |
27 | | #include <cmath> |
28 | | #include <csignal> |
29 | | #include <random> |
30 | | #include <thread> |
31 | | |
32 | | namespace hermes { |
33 | | namespace vm { |
34 | | |
35 | 0 | void SamplingProfiler::registerDomain(Domain *domain) { |
36 | | // If domain is not already registered, add it to the list. |
37 | 0 | auto it = std::find(domains_.begin(), domains_.end(), domain); |
38 | 0 | if (it == domains_.end()) |
39 | 0 | domains_.push_back(domain); |
40 | 0 | } |
41 | | |
42 | | SamplingProfiler::NativeFunctionFrameInfo |
43 | 0 | SamplingProfiler::registerNativeFunction(NativeFunction *nativeFunction) { |
44 | | // If nativeFunction is not already registered, add it to the list. |
45 | 0 | auto it = std::find( |
46 | 0 | nativeFunctions_.begin(), nativeFunctions_.end(), nativeFunction); |
47 | 0 | if (it != nativeFunctions_.end()) { |
48 | 0 | return it - nativeFunctions_.begin(); |
49 | 0 | } |
50 | | |
51 | 0 | nativeFunctions_.push_back(nativeFunction); |
52 | 0 | return nativeFunctions_.size() - 1; |
53 | 0 | } |
54 | | |
55 | 0 | void SamplingProfiler::markRootsForCompleteMarking(RootAcceptor &acceptor) { |
56 | 0 | std::lock_guard<std::mutex> lockGuard(runtimeDataLock_); |
57 | 0 | for (Domain *&domain : domains_) { |
58 | 0 | acceptor.acceptPtr(domain); |
59 | 0 | } |
60 | 0 | } |
61 | | |
62 | 0 | void SamplingProfiler::markRoots(RootAcceptor &acceptor) { |
63 | 0 | std::lock_guard<std::mutex> lockGuard(runtimeDataLock_); |
64 | 0 | for (Domain *&domain : domains_) { |
65 | 0 | acceptor.acceptPtr(domain); |
66 | 0 | } |
67 | |
|
68 | 0 | for (NativeFunction *&fn : nativeFunctions_) { |
69 | 0 | acceptor.acceptPtr(fn); |
70 | 0 | } |
71 | 0 | } |
72 | | |
73 | | uint32_t SamplingProfiler::walkRuntimeStack( |
74 | | StackTrace &sampleStorage, |
75 | | InLoom inLoom, |
76 | 0 | uint32_t startIndex) { |
77 | 0 | unsigned count = startIndex; |
78 | | |
79 | | // TODO: capture leaf frame IP. |
80 | 0 | const Inst *ip = nullptr; |
81 | 0 | for (ConstStackFramePtr frame : runtime_.getStackFrames()) { |
82 | | // Whether we successfully captured a stack frame or not. |
83 | 0 | bool capturedFrame = true; |
84 | 0 | auto &frameStorage = sampleStorage.stack[count]; |
85 | | // Check if it is pure JS frame. |
86 | 0 | auto *calleeCodeBlock = frame.getCalleeCodeBlock(runtime_); |
87 | 0 | if (calleeCodeBlock != nullptr) { |
88 | 0 | frameStorage.kind = StackFrame::FrameKind::JSFunction; |
89 | 0 | frameStorage.jsFrame.functionId = calleeCodeBlock->getFunctionID(); |
90 | 0 | frameStorage.jsFrame.offset = |
91 | 0 | (ip == nullptr ? 0 : calleeCodeBlock->getOffsetOf(ip)); |
92 | 0 | auto *module = calleeCodeBlock->getRuntimeModule(); |
93 | 0 | assert(module != nullptr && "Cannot fetch runtimeModule for code block"); |
94 | 0 | frameStorage.jsFrame.module = module; |
95 | | // Don't execute a read or write barrier here because this is a signal |
96 | | // handler. |
97 | 0 | if (inLoom != InLoom::Yes) |
98 | 0 | registerDomain(module->getDomainForSamplingProfiler(runtime_)); |
99 | 0 | } else if ( |
100 | 0 | auto *nativeFunction = |
101 | 0 | dyn_vmcast<NativeFunction>(frame.getCalleeClosureUnsafe())) { |
102 | 0 | frameStorage.kind = vmisa<FinalizableNativeFunction>(nativeFunction) |
103 | 0 | ? StackFrame::FrameKind::FinalizableNativeFunction |
104 | 0 | : StackFrame::FrameKind::NativeFunction; |
105 | 0 | if (inLoom != InLoom::Yes) { |
106 | 0 | frameStorage.nativeFrame = registerNativeFunction(nativeFunction); |
107 | 0 | } else { |
108 | 0 | frameStorage.nativeFunctionPtrForLoom = |
109 | 0 | nativeFunction->getFunctionPtr(); |
110 | 0 | } |
111 | 0 | } else { |
112 | | // TODO: handle BoundFunction. |
113 | 0 | capturedFrame = false; |
114 | 0 | } |
115 | | |
116 | | // Update ip to caller for next iteration. |
117 | 0 | ip = frame.getSavedIP(); |
118 | 0 | if (capturedFrame) { |
119 | 0 | ++count; |
120 | 0 | if (count >= sampleStorage.stack.size()) { |
121 | 0 | break; |
122 | 0 | } |
123 | 0 | } |
124 | 0 | } |
125 | 0 | sampleStorage.tid = threadID_; |
126 | 0 | sampleStorage.timeStamp = std::chrono::steady_clock::now(); |
127 | 0 | return count; |
128 | 0 | } |
129 | | |
130 | | SamplingProfiler::SamplingProfiler(Runtime &runtime) |
131 | 0 | : threadID_{oscompat::global_thread_id()}, runtime_{runtime} { |
132 | 0 | threadNames_[threadID_] = oscompat::thread_name(); |
133 | 0 | sampling_profiler::Sampler::get()->registerRuntime(this); |
134 | 0 | } |
135 | | |
136 | 0 | void SamplingProfiler::dumpSampledStackGlobal(llvh::raw_ostream &OS) { |
137 | 0 | auto globalProfiler = sampling_profiler::Sampler::get(); |
138 | 0 | std::lock_guard<std::mutex> lk(globalProfiler->profilerLock_); |
139 | 0 | if (!globalProfiler->profilers_.empty()) { |
140 | 0 | auto *localProfiler = *globalProfiler->profilers_.begin(); |
141 | 0 | localProfiler->dumpSampledStack(OS); |
142 | 0 | } |
143 | 0 | } |
144 | | |
145 | 0 | void SamplingProfiler::dumpSampledStack(llvh::raw_ostream &OS) { |
146 | 0 | std::lock_guard<std::mutex> lk(runtimeDataLock_); |
147 | 0 | OS << "dumpSamples called from runtime\n"; |
148 | 0 | OS << "Total " << sampledStacks_.size() << " samples\n"; |
149 | 0 | for (unsigned i = 0; i < sampledStacks_.size(); ++i) { |
150 | 0 | auto &sample = sampledStacks_[i]; |
151 | 0 | uint64_t timeStamp = sample.timeStamp.time_since_epoch().count(); |
152 | 0 | OS << "[" << i << "]: tid[" << sample.tid << "], ts[" << timeStamp << "] "; |
153 | | |
154 | | // Leaf frame is in sample[0] so dump it backward to |
155 | | // get root => leaf represenation. |
156 | 0 | for (auto iter = sample.stack.rbegin(); iter != sample.stack.rend(); |
157 | 0 | ++iter) { |
158 | 0 | const StackFrame &frame = *iter; |
159 | 0 | switch (frame.kind) { |
160 | 0 | case StackFrame::FrameKind::JSFunction: |
161 | 0 | OS << "[JS] " << frame.jsFrame.functionId << ":" |
162 | 0 | << frame.jsFrame.offset; |
163 | 0 | break; |
164 | | |
165 | 0 | case StackFrame::FrameKind::NativeFunction: { |
166 | 0 | NativeFunctionPtr nativeFrame = getNativeFunctionPtr(frame); |
167 | 0 | OS << "[Native] " << reinterpret_cast<uintptr_t>(nativeFrame); |
168 | 0 | break; |
169 | 0 | } |
170 | | |
171 | 0 | case StackFrame::FrameKind::FinalizableNativeFunction: |
172 | 0 | OS << "[HostFunction] " << getNativeFunctionName(frame); |
173 | 0 | break; |
174 | | |
175 | 0 | default: |
176 | 0 | llvm_unreachable("Unknown frame kind"); |
177 | 0 | } |
178 | 0 | OS << " => "; |
179 | 0 | } |
180 | 0 | OS << "\n"; |
181 | 0 | } |
182 | 0 | } |
183 | | |
184 | 0 | void SamplingProfiler::dumpChromeTraceGlobal(llvh::raw_ostream &OS) { |
185 | 0 | auto globalProfiler = sampling_profiler::Sampler::get(); |
186 | 0 | std::lock_guard<std::mutex> lk(globalProfiler->profilerLock_); |
187 | 0 | if (!globalProfiler->profilers_.empty()) { |
188 | 0 | auto *localProfiler = *globalProfiler->profilers_.begin(); |
189 | 0 | localProfiler->dumpChromeTrace(OS); |
190 | 0 | } |
191 | 0 | } |
192 | | |
193 | 0 | void SamplingProfiler::dumpChromeTrace(llvh::raw_ostream &OS) { |
194 | 0 | std::lock_guard<std::mutex> lk(runtimeDataLock_); |
195 | 0 | auto pid = oscompat::process_id(); |
196 | 0 | ChromeTraceSerializer serializer( |
197 | 0 | *this, ChromeTraceFormat::create(pid, threadNames_, sampledStacks_)); |
198 | 0 | serializer.serialize(OS); |
199 | 0 | clear(); |
200 | 0 | } |
201 | | |
202 | 0 | void SamplingProfiler::serializeInDevToolsFormat(llvh::raw_ostream &OS) { |
203 | 0 | std::lock_guard<std::mutex> lk(runtimeDataLock_); |
204 | 0 | hermes::vm::serializeAsProfilerProfile( |
205 | 0 | *this, |
206 | 0 | OS, |
207 | 0 | ChromeTraceFormat::create( |
208 | 0 | oscompat::process_id(), threadNames_, sampledStacks_)); |
209 | 0 | clear(); |
210 | 0 | } |
211 | | |
212 | 0 | facebook::hermes::sampling_profiler::Profile SamplingProfiler::dumpAsProfile() { |
213 | 0 | std::lock_guard<std::mutex> lk(runtimeDataLock_); |
214 | |
|
215 | 0 | facebook::hermes::sampling_profiler::Profile profile = |
216 | 0 | generateProfile(*this, sampledStacks_); |
217 | |
|
218 | 0 | clear(); |
219 | 0 | return profile; |
220 | 0 | } |
221 | | |
222 | 0 | bool SamplingProfiler::enable(double meanHzFreq) { |
223 | 0 | return sampling_profiler::Sampler::get()->enable(meanHzFreq); |
224 | 0 | } |
225 | | |
226 | 0 | bool SamplingProfiler::disable() { |
227 | 0 | return sampling_profiler::Sampler::get()->disable(); |
228 | 0 | } |
229 | | |
230 | 0 | void SamplingProfiler::clear() { |
231 | 0 | sampledStacks_.clear(); |
232 | | // Release all strong roots. |
233 | 0 | domains_.clear(); |
234 | 0 | nativeFunctions_.clear(); |
235 | 0 | } |
236 | | |
237 | | void SamplingProfiler::suspend( |
238 | | SuspendFrameInfo::Kind reason, |
239 | 0 | llvh::StringRef gcSuspendDetails) { |
240 | | // Need to check whether the profiler is enabled without holding the |
241 | | // runtimeDataLock_. Otherwise, we'd have a lock inversion. |
242 | 0 | bool enabled = sampling_profiler::Sampler::get()->enabled(); |
243 | |
|
244 | 0 | std::lock_guard<std::mutex> lk(runtimeDataLock_); |
245 | 0 | if (++suspendCount_ > 1) { |
246 | | // If there are multiple nested suspend calls use a default "multiple" frame |
247 | | // kind for the suspend entry in the call stack. |
248 | 0 | reason = SuspendFrameInfo::Kind::Multiple; |
249 | 0 | } else if (reason == SuspendFrameInfo::Kind::GC && gcSuspendDetails.empty()) { |
250 | | // If no GC reason was provided, use a default "suspend" value. |
251 | 0 | gcSuspendDetails = "suspend"; |
252 | 0 | } |
253 | | |
254 | | // Only record the stack trace for the first suspend() call. |
255 | 0 | if (LLVM_UNLIKELY(enabled && suspendCount_ == 1)) { |
256 | 0 | recordPreSuspendStack(reason, gcSuspendDetails); |
257 | 0 | } |
258 | 0 | } |
259 | | |
260 | 0 | void SamplingProfiler::resume() { |
261 | 0 | std::lock_guard<std::mutex> lk(runtimeDataLock_); |
262 | 0 | assert(suspendCount_ > 0 && "resume() without suspend()"); |
263 | 0 | if (--suspendCount_ == 0) { |
264 | 0 | preSuspendStackDepth_ = 0; |
265 | 0 | } |
266 | 0 | } |
267 | | |
268 | | void SamplingProfiler::recordPreSuspendStack( |
269 | | SuspendFrameInfo::Kind reason, |
270 | 0 | llvh::StringRef gcFrame) { |
271 | 0 | SuspendFrameInfo suspendExtraInfo{reason, nullptr}; |
272 | 0 | if (reason == SuspendFrameInfo::Kind::GC) { |
273 | 0 | std::pair<std::unordered_set<std::string>::iterator, bool> retPair = |
274 | 0 | gcEventExtraInfoSet_.emplace(gcFrame); |
275 | 0 | suspendExtraInfo.gcFrame = &(*(retPair.first)); |
276 | 0 | } |
277 | |
|
278 | 0 | auto &leafFrame = preSuspendStackStorage_.stack[0]; |
279 | 0 | leafFrame.kind = StackFrame::FrameKind::SuspendFrame; |
280 | 0 | leafFrame.suspendFrame = suspendExtraInfo; |
281 | | |
282 | | // Leaf frame slot has been used, filling from index 1. |
283 | 0 | preSuspendStackDepth_ = |
284 | 0 | walkRuntimeStack(preSuspendStackStorage_, InLoom::No, 1); |
285 | 0 | } |
286 | | |
287 | | bool operator==( |
288 | | const SamplingProfiler::StackFrame &left, |
289 | 0 | const SamplingProfiler::StackFrame &right) { |
290 | 0 | if (left.kind != right.kind) { |
291 | 0 | return false; |
292 | 0 | } |
293 | 0 | switch (left.kind) { |
294 | 0 | case SamplingProfiler::StackFrame::FrameKind::JSFunction: |
295 | 0 | return left.jsFrame.functionId == right.jsFrame.functionId && |
296 | 0 | left.jsFrame.offset == right.jsFrame.offset; |
297 | | |
298 | 0 | case SamplingProfiler::StackFrame::FrameKind::NativeFunction: |
299 | 0 | case SamplingProfiler::StackFrame::FrameKind::FinalizableNativeFunction: |
300 | 0 | return left.nativeFrame == right.nativeFrame; |
301 | | |
302 | 0 | case SamplingProfiler::StackFrame::FrameKind::SuspendFrame: |
303 | 0 | return left.suspendFrame.kind == right.suspendFrame.kind && |
304 | 0 | left.suspendFrame.gcFrame == right.suspendFrame.gcFrame; |
305 | | |
306 | 0 | default: |
307 | 0 | llvm_unreachable("Unknown frame kind"); |
308 | 0 | } |
309 | 0 | } |
310 | | |
311 | | } // namespace vm |
312 | | } // namespace hermes |
313 | | |
314 | | #endif // HERMESVM_SAMPLING_PROFILER_AVAILABLE |