/src/hermes/lib/VM/Profiler/SamplingProfilerSampler.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 "SamplingProfilerSampler.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 | | |
22 | | #include <fcntl.h> |
23 | | #include <cassert> |
24 | | #include <chrono> |
25 | | #include <cmath> |
26 | | #include <csignal> |
27 | | #include <random> |
28 | | #include <thread> |
29 | | |
30 | | namespace hermes { |
31 | | namespace vm { |
32 | | namespace sampling_profiler { |
33 | | |
34 | 0 | Sampler::~Sampler() = default; |
35 | | |
36 | 0 | void Sampler::registerRuntime(SamplingProfiler *profiler) { |
37 | 0 | std::lock_guard<std::mutex> lockGuard(profilerLock_); |
38 | 0 | profilers_.insert(profiler); |
39 | 0 | platformRegisterRuntime(profiler); |
40 | 0 | } |
41 | | |
42 | 0 | void Sampler::unregisterRuntime(SamplingProfiler *profiler) { |
43 | 0 | std::lock_guard<std::mutex> lockGuard(profilerLock_); |
44 | 0 | bool succeed = profilers_.erase(profiler); |
45 | | // TODO: should we allow recursive style |
46 | | // register/register -> unregister/unregister call? |
47 | 0 | assert(succeed && "How can runtime not registered yet?"); |
48 | 0 | (void)succeed; |
49 | 0 | platformUnregisterRuntime(profiler); |
50 | 0 | } |
51 | | |
52 | 0 | bool Sampler::sampleStacks() { |
53 | 0 | for (SamplingProfiler *localProfiler : profilers_) { |
54 | 0 | std::lock_guard<std::mutex> lk(localProfiler->runtimeDataLock_); |
55 | 0 | if (!sampleStack(localProfiler)) { |
56 | 0 | return false; |
57 | 0 | } |
58 | 0 | platformPostSampleStack(localProfiler); |
59 | 0 | } |
60 | 0 | return true; |
61 | 0 | } |
62 | | |
63 | 0 | bool Sampler::sampleStack(SamplingProfiler *localProfiler) { |
64 | 0 | if (localProfiler->suspendCount_ > 0) { |
65 | | // Sampling profiler is suspended. Copy pre-captured stack instead without |
66 | | // interrupting the VM thread. |
67 | 0 | if (localProfiler->preSuspendStackDepth_ > 0) { |
68 | 0 | sampleStorage_ = localProfiler->preSuspendStackStorage_; |
69 | 0 | sampledStackDepth_ = localProfiler->preSuspendStackDepth_; |
70 | 0 | } else { |
71 | | // This suspension didn't record a stack trace. For example, a GC (like |
72 | | // mallocGC) did not record JS stack. |
73 | | // TODO: fix this for all cases. |
74 | 0 | sampledStackDepth_ = 0; |
75 | 0 | } |
76 | 0 | } else { |
77 | | // Ensure there are no allocations in the signal handler by keeping ample |
78 | | // reserved space. |
79 | 0 | localProfiler->domains_.reserve( |
80 | 0 | localProfiler->domains_.size() + SamplingProfiler::kMaxStackDepth); |
81 | 0 | size_t domainCapacityBefore = localProfiler->domains_.capacity(); |
82 | 0 | (void)domainCapacityBefore; |
83 | | |
84 | | // Ditto for native functions. |
85 | 0 | localProfiler->nativeFunctions_.reserve( |
86 | 0 | localProfiler->nativeFunctions_.size() + |
87 | 0 | SamplingProfiler::kMaxStackDepth); |
88 | 0 | size_t nativeFunctionsCapacityBefore = |
89 | 0 | localProfiler->nativeFunctions_.capacity(); |
90 | 0 | (void)nativeFunctionsCapacityBefore; |
91 | |
|
92 | 0 | if (!platformSuspendVMAndWalkStack(localProfiler)) { |
93 | 0 | return false; |
94 | 0 | } |
95 | | |
96 | 0 | assert( |
97 | 0 | localProfiler->domains_.capacity() == domainCapacityBefore && |
98 | 0 | "Must not dynamically allocate in signal handler"); |
99 | | |
100 | 0 | assert( |
101 | 0 | localProfiler->nativeFunctions_.capacity() == |
102 | 0 | nativeFunctionsCapacityBefore && |
103 | 0 | "Must not dynamically allocate in signal handler"); |
104 | 0 | } |
105 | | |
106 | 0 | assert( |
107 | 0 | sampledStackDepth_ <= sampleStorage_.stack.size() && |
108 | 0 | "How can we sample more frames than storage?"); |
109 | 0 | localProfiler->sampledStacks_.emplace_back( |
110 | 0 | sampleStorage_.tid, |
111 | 0 | sampleStorage_.timeStamp, |
112 | 0 | sampleStorage_.stack.begin(), |
113 | 0 | sampleStorage_.stack.begin() + sampledStackDepth_); |
114 | 0 | return true; |
115 | 0 | } |
116 | | |
117 | 0 | void Sampler::walkRuntimeStack(SamplingProfiler *profiler) { |
118 | 0 | assert( |
119 | 0 | profiler->suspendCount_ == 0 && |
120 | 0 | "Shouldn't interrupt the VM thread when the sampling profiler is " |
121 | 0 | "suspended."); |
122 | | |
123 | | // Sampling stack will touch GC objects(like closure) so only do so if heap |
124 | | // is valid. |
125 | 0 | auto &curThreadRuntime = profiler->runtime_; |
126 | 0 | assert( |
127 | 0 | !curThreadRuntime.getHeap().inGC() && |
128 | 0 | "sampling profiler should be suspended before GC"); |
129 | 0 | (void)curThreadRuntime; |
130 | 0 | sampledStackDepth_ = |
131 | 0 | profiler->walkRuntimeStack(sampleStorage_, SamplingProfiler::InLoom::No); |
132 | 0 | } |
133 | | |
134 | 0 | void Sampler::timerLoop() { |
135 | 0 | oscompat::set_thread_name("hermes-sampling-profiler"); |
136 | |
|
137 | 0 | constexpr double kMeanMilliseconds = 10; |
138 | 0 | constexpr double kStdDevMilliseconds = 5; |
139 | 0 | std::random_device rd{}; |
140 | 0 | std::mt19937 gen{rd()}; |
141 | | // The amount of time that is spent sleeping comes from a normal distribution, |
142 | | // to avoid the case where the timer thread samples a stack at a predictable |
143 | | // period. |
144 | 0 | std::normal_distribution<> distribution{ |
145 | 0 | kMeanMilliseconds, kStdDevMilliseconds}; |
146 | 0 | std::unique_lock<std::mutex> uniqueLock(profilerLock_); |
147 | |
|
148 | 0 | while (enabled_) { |
149 | 0 | if (!sampleStacks()) { |
150 | 0 | return; |
151 | 0 | } |
152 | | |
153 | 0 | const uint64_t millis = round(std::fabs(distribution(gen))); |
154 | | // TODO: make sampling rate configurable. |
155 | 0 | enabledCondVar_.wait_for( |
156 | 0 | uniqueLock, std::chrono::milliseconds(millis), [this]() { |
157 | 0 | return !enabled_; |
158 | 0 | }); |
159 | 0 | } |
160 | 0 | } |
161 | | |
162 | 0 | bool Sampler::enabled() { |
163 | 0 | std::lock_guard<std::mutex> lockGuard(profilerLock_); |
164 | 0 | return enabled_; |
165 | 0 | } |
166 | | |
167 | 0 | bool Sampler::enable() { |
168 | 0 | std::lock_guard<std::mutex> lockGuard(profilerLock_); |
169 | 0 | if (enabled_) { |
170 | 0 | return true; |
171 | 0 | } |
172 | 0 | if (!platformEnable()) { |
173 | 0 | return false; |
174 | 0 | } |
175 | 0 | enabled_ = true; |
176 | | // Start timer thread. |
177 | 0 | timerThread_ = std::thread(&Sampler::timerLoop, this); |
178 | 0 | return true; |
179 | 0 | } |
180 | | |
181 | 0 | bool Sampler::disable() { |
182 | 0 | { |
183 | 0 | std::lock_guard<std::mutex> lockGuard(profilerLock_); |
184 | 0 | if (!enabled_) { |
185 | | // Already disabled. |
186 | 0 | return true; |
187 | 0 | } |
188 | 0 | if (!platformDisable()) { |
189 | 0 | return false; |
190 | 0 | } |
191 | | // Telling timer thread to exit. |
192 | 0 | enabled_ = false; |
193 | 0 | } |
194 | | // Notify the timer thread that it has been disabled. |
195 | 0 | enabledCondVar_.notify_all(); |
196 | | // Wait for timer thread to exit. This avoids the timer thread reading from |
197 | | // memory that is freed after a main thread exits. This is outside the lock |
198 | | // on profilerLock_ since the timer thread needs to acquire that lock. |
199 | 0 | timerThread_.join(); |
200 | 0 | return true; |
201 | 0 | } |
202 | | |
203 | | } // namespace sampling_profiler |
204 | | } // namespace vm |
205 | | } // namespace hermes |
206 | | |
207 | | #endif // HERMESVM_SAMPLING_PROFILER_AVAILABLE |