blob: 820a1c98f8a4229b61b681326d7517caa9c80221 [file] [log] [blame]
Avi Drissmane4622aa2022-09-08 20:36:061// Copyright 2018 The Chromium Authors
Alexei Filippovd6363e472018-08-27 19:31:392// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#ifndef BASE_SAMPLING_HEAP_PROFILER_POISSON_ALLOCATION_SAMPLER_H_
6#define BASE_SAMPLING_HEAP_PROFILER_POISSON_ALLOCATION_SAMPLER_H_
7
Joe Mason0119d492023-01-19 06:31:498#include <atomic>
Joe Mason74ce1242025-03-24 16:48:329#include <optional>
Alexei Filippovd6363e472018-08-27 19:31:3910#include <vector>
11
André Kempe69aec282024-01-03 19:28:4512#include "base/allocator/dispatcher/notification_data.h"
André Kempe1f9ea582023-01-25 14:35:0313#include "base/allocator/dispatcher/reentry_guard.h"
André Kempeb99648e2023-01-06 17:21:2014#include "base/allocator/dispatcher/subsystem.h"
Alexei Filippovd6363e472018-08-27 19:31:3915#include "base/base_export.h"
Alexei Filippov02435742019-06-05 04:43:0116#include "base/compiler_specific.h"
Joe Masond9f877d2021-11-30 20:27:2817#include "base/gtest_prod_util.h"
Arthur Sonzognib8e46e92024-05-31 08:01:1918#include "base/memory/raw_ptr_exclusion.h"
Avi Drissmanded77172021-07-02 18:23:0019#include "base/no_destructor.h"
Alexei Filippov02435742019-06-05 04:43:0120#include "base/sampling_heap_profiler/lock_free_address_hash_set.h"
Alexei Filippovd6363e472018-08-27 19:31:3921#include "base/synchronization/lock.h"
Alexei Filippovf071fd42019-09-11 21:37:3422#include "base/thread_annotations.h"
Alexei Filippovd6363e472018-08-27 19:31:3923
24namespace base {
25
Joe Masond9f877d2021-11-30 20:27:2826class SamplingHeapProfilerTest;
27
Joe Masonbc586dc2024-12-11 18:28:5228// Stats about the allocation sampler.
29struct BASE_EXPORT PoissonAllocationSamplerStats {
Joe Masondf3cdbc2025-06-18 20:07:0530 using AddressCacheBucketStats = LockFreeAddressHashSet::BucketStats;
31
Joe Masoncf0e2e12024-12-11 22:09:4732 PoissonAllocationSamplerStats(
33 size_t address_cache_hits,
34 size_t address_cache_misses,
35 size_t address_cache_max_size,
36 float address_cache_max_load_factor,
Joe Masondf3cdbc2025-06-18 20:07:0537 AddressCacheBucketStats address_cache_bucket_stats);
Joe Masoncf0e2e12024-12-11 22:09:4738 ~PoissonAllocationSamplerStats();
39
40 PoissonAllocationSamplerStats(const PoissonAllocationSamplerStats&);
41 PoissonAllocationSamplerStats& operator=(
42 const PoissonAllocationSamplerStats&);
43
44 size_t address_cache_hits;
45 size_t address_cache_misses;
46 size_t address_cache_max_size;
47 float address_cache_max_load_factor;
Joe Masondf3cdbc2025-06-18 20:07:0548 AddressCacheBucketStats address_cache_bucket_stats;
Joe Masonbc586dc2024-12-11 18:28:5249};
50
Alexei Filippovd6363e472018-08-27 19:31:3951// This singleton class implements Poisson sampling of the incoming allocations
52// stream. It hooks onto base::allocator and base::PartitionAlloc.
Alexei Filippovd6363e472018-08-27 19:31:3953// The only control parameter is sampling interval that controls average value
54// of the sampling intervals. The actual intervals between samples are
55// randomized using Poisson distribution to mitigate patterns in the allocation
56// stream.
57// Once accumulated allocation sizes fill up the current sample interval,
58// a sample is generated and sent to the observers via |SampleAdded| call.
59// When the corresponding memory that triggered the sample is freed observers
60// get notified with |SampleRemoved| call.
61//
62class BASE_EXPORT PoissonAllocationSampler {
63 public:
Alexei Filippovd6363e472018-08-27 19:31:3964 class SamplesObserver {
65 public:
66 virtual ~SamplesObserver() = default;
André Kempeb99648e2023-01-06 17:21:2067 virtual void SampleAdded(
68 void* address,
69 size_t size,
70 size_t total,
71 base::allocator::dispatcher::AllocationSubsystem type,
72 const char* context) = 0;
Alexei Filippovd6363e472018-08-27 19:31:3973 virtual void SampleRemoved(void* address) = 0;
74 };
75
Joe Masond9f877d2021-11-30 20:27:2876 // An instance of this class makes the sampler not report samples generated
Alexei Filippov28cc68d2018-09-13 07:58:3677 // within the object scope for the current thread.
78 // It allows observers to allocate/deallocate memory while holding a lock
79 // without a chance to get into reentrancy problems.
Alexei Filippovbc379632018-09-14 22:29:0680 class BASE_EXPORT ScopedMuteThreadSamples {
Alexei Filippov28cc68d2018-09-13 07:58:3681 public:
Alexei Filippovbc379632018-09-14 22:29:0682 ScopedMuteThreadSamples();
83 ~ScopedMuteThreadSamples();
Alexei Filippove48985e2019-02-01 00:27:4184
Joe Masond9f877d2021-11-30 20:27:2885 ScopedMuteThreadSamples(const ScopedMuteThreadSamples&) = delete;
86 ScopedMuteThreadSamples& operator=(const ScopedMuteThreadSamples&) = delete;
87
Alexei Filippove48985e2019-02-01 00:27:4188 static bool IsMuted();
Joe Masonbb9554192025-01-09 19:21:0089
90 private:
91 bool was_muted_ = false;
Alexei Filippov28cc68d2018-09-13 07:58:3692 };
93
Joe Masonf18dd4a2022-05-02 18:41:5194 // An instance of this class makes the sampler behave deterministically to
95 // ensure test results are repeatable. Does not support nesting.
96 class BASE_EXPORT ScopedSuppressRandomnessForTesting {
97 public:
98 ScopedSuppressRandomnessForTesting();
99 ~ScopedSuppressRandomnessForTesting();
100
101 ScopedSuppressRandomnessForTesting(
102 const ScopedSuppressRandomnessForTesting&) = delete;
103 ScopedSuppressRandomnessForTesting& operator=(
104 const ScopedSuppressRandomnessForTesting&) = delete;
105
106 static bool IsSuppressed();
107 };
108
Joe Mason5fb082b42024-05-14 18:34:35109 // An instance of this class makes the sampler only report samples with
110 // AllocatorType kManualForTesting, not those from hooked allocators. This
111 // allows unit tests to set test expectations based on only explicit calls to
112 // RecordAlloc and RecordFree.
113 //
114 // The accumulated bytes on the thread that creates a
115 // ScopedMuteHookedSamplesForTesting will also be reset to 0, and restored
116 // when the object leaves scope. This gives tests a known state to start
117 // recording samples on one thread: a full interval must pass to record a
118 // sample. Other threads will still have a random number of accumulated bytes.
119 //
120 // Only one instance may exist at a time.
121 class BASE_EXPORT ScopedMuteHookedSamplesForTesting {
122 public:
123 ScopedMuteHookedSamplesForTesting();
124 ~ScopedMuteHookedSamplesForTesting();
125
126 // Move-only.
127 ScopedMuteHookedSamplesForTesting(
128 const ScopedMuteHookedSamplesForTesting&) = delete;
129 ScopedMuteHookedSamplesForTesting& operator=(
130 const ScopedMuteHookedSamplesForTesting&) = delete;
131 ScopedMuteHookedSamplesForTesting(ScopedMuteHookedSamplesForTesting&&);
132 ScopedMuteHookedSamplesForTesting& operator=(
133 ScopedMuteHookedSamplesForTesting&&);
134
135 private:
136 intptr_t accumulated_bytes_snapshot_;
137 };
138
Alexei Filippovd6363e472018-08-27 19:31:39139 // Must be called early during the process initialization. It creates and
140 // reserves a TLS slot.
141 static void Init();
142
Alexei Filippovd6363e472018-08-27 19:31:39143 void AddSamplesObserver(SamplesObserver*);
Alexei Filippovf071fd42019-09-11 21:37:34144
145 // Note: After an observer is removed it is still possible to receive
146 // a notification to that observer. This is not a problem currently as
147 // the only client of this interface is the base::SamplingHeapProfiler,
148 // which is a singleton.
149 // If there's a need for this functionality in the future, one might
150 // want to put observers notification loop under a reader-writer lock.
Alexei Filippovd6363e472018-08-27 19:31:39151 void RemoveSamplesObserver(SamplesObserver*);
152
Joe Masonaf0833ca2022-01-28 21:49:48153 // Sets the mean number of bytes that will be allocated before taking a
154 // sample.
155 void SetSamplingInterval(size_t sampling_interval_bytes);
156
157 // Returns the current mean sampling interval, in bytes.
158 size_t SamplingInterval() const;
159
Joe Mason74ce1242025-03-24 16:48:32160 // Sets the max load factor before rebalancing the LockFreeAddressHashSet, or
161 // resets it to the default if `load_factor` is nulloptr.
162 void SetTargetHashSetLoadFactor(std::optional<float> load_factor);
163
Joe Masonbc586dc2024-12-11 18:28:52164 // Returns statistics about the allocation sampler, and resets the running
165 // counts so that each call to this returns only stats about the period
166 // between calls.
167 PoissonAllocationSamplerStats GetAndResetStats();
168
André Kempe543fe172023-04-05 19:40:05169 ALWAYS_INLINE void OnAllocation(
André Kempe69aec282024-01-03 19:28:45170 const base::allocator::dispatcher::AllocationNotificationData&
171 allocation_data);
172 ALWAYS_INLINE void OnFree(
173 const base::allocator::dispatcher::FreeNotificationData& free_data);
André Kempef3959c072023-01-19 19:49:29174
Alexei Filippovd6363e472018-08-27 19:31:39175 static PoissonAllocationSampler* Get();
176
Peter Boström75cd3c02021-09-28 15:23:18177 PoissonAllocationSampler(const PoissonAllocationSampler&) = delete;
178 PoissonAllocationSampler& operator=(const PoissonAllocationSampler&) = delete;
179
Joe Mason5fb082b42024-05-14 18:34:35180 // Returns true if a ScopedMuteHookedSamplesForTesting exists. This can be
181 // read from any thread.
Joe Mason0119d492023-01-19 06:31:49182 static bool AreHookedSamplesMuted() {
183 return profiling_state_.load(std::memory_order_relaxed) &
184 ProfilingStateFlag::kHookedSamplesMutedForTesting;
185 }
Joe Masond9f877d2021-11-30 20:27:28186
Joe Masonbb9554192025-01-09 19:21:00187 // Returns the number of allocated bytes that have been observed.
188 static intptr_t GetAccumulatedBytesForTesting();
189
Alexei Filippovd6363e472018-08-27 19:31:39190 private:
Joe Mason0119d492023-01-19 06:31:49191 // Flags recording the state of the profiler. This does not use enum class so
192 // flags can be used in a bitmask.
193 enum ProfilingStateFlag {
194 // Set if profiling has ever been started in this session of Chrome. Once
195 // this is set, it is never reset. This is used to optimize the common case
196 // where profiling is never used.
197 kWasStarted = 1 << 0,
198 // Set if profiling is currently running. This flag is toggled on and off
199 // as sample observers are added and removed.
200 kIsRunning = 1 << 1,
201 // Set if a ScopedMuteHookedSamplesForTesting object exists.
202 kHookedSamplesMutedForTesting = 1 << 2,
203 };
204 using ProfilingStateFlagMask = int;
205
Alexei Filippovd6363e472018-08-27 19:31:39206 PoissonAllocationSampler();
207 ~PoissonAllocationSampler() = delete;
208
Alexei Filippovd6363e472018-08-27 19:31:39209 static size_t GetNextSampleInterval(size_t base_interval);
Joe Masonea96b4e2022-04-26 16:07:42210
211 // Return the set of sampled addresses. This is only valid to call after
212 // Init().
Alexei Filippovd6363e472018-08-27 19:31:39213 static LockFreeAddressHashSet& sampled_addresses_set();
214
Joe Mason0119d492023-01-19 06:31:49215 // Atomically adds `flag` to `profiling_state_`. DCHECK's if it was already
216 // set. If `flag` is kIsRunning, also sets kWasStarted. Uses
217 // std::memory_order_relaxed semantics and therefore doesn't synchronize the
218 // state of any other memory with future readers. (See the comment in
219 // RecordFree() for why this is safe.)
220 static void SetProfilingStateFlag(ProfilingStateFlag flag);
221
222 // Atomically removes `flag` from `profiling_state_`. DCHECK's if it was not
223 // already set. Uses std::memory_order_relaxed semantics and therefore doesn't
224 // synchronize the state of any other memory with future readers. (See the
225 // comment in RecordFree() for why this is safe.)
226 static void ResetProfilingStateFlag(ProfilingStateFlag flag);
227
André Kempef3959c072023-01-19 19:49:29228 void DoRecordAllocation(const ProfilingStateFlagMask state,
229 void* address,
230 size_t size,
231 base::allocator::dispatcher::AllocationSubsystem type,
232 const char* context);
Alexei Filippovd6363e472018-08-27 19:31:39233 void DoRecordFree(void* address);
234
Joe Mason74ce1242025-03-24 16:48:32235 void BalanceAddressesHashSet() EXCLUSIVE_LOCKS_REQUIRED(mutex_);
Alexei Filippovd6363e472018-08-27 19:31:39236
Alexei Filippovd6363e472018-08-27 19:31:39237 Lock mutex_;
Joe Mason0119d492023-01-19 06:31:49238
Alexei Filippovf071fd42019-09-11 21:37:34239 // The |observers_| list is guarded by |mutex_|, however a copy of it
240 // is made before invoking the observers (to avoid performing expensive
241 // operations under the lock) as such the SamplesObservers themselves need
242 // to be thread-safe and support being invoked racily after
243 // RemoveSamplesObserver().
Arthur Sonzognib8e46e92024-05-31 08:01:19244 //
245 // This class handles allocation, so it must never use raw_ptr<T>. In
246 // particular, raw_ptr<T> with `enable_backup_ref_ptr_instance_tracer`
247 // developer option allocates memory, which would cause reentrancy issues:
248 // allocating memory while allocating memory.
249 // More details in https://crbug.com/340815319
250 RAW_PTR_EXCLUSION std::vector<SamplesObserver*> observers_ GUARDED_BY(mutex_);
Alexei Filippovd6363e472018-08-27 19:31:39251
Joe Mason0119d492023-01-19 06:31:49252 // Fast, thread-safe access to the current profiling state.
253 static std::atomic<ProfilingStateFlagMask> profiling_state_;
254
Joe Masonbc586dc2024-12-11 18:28:52255 // Running counts for PoissonAllocationSamplerStats. These are all atomic or
256 // mutex-guarded because they're updated from multiple threads. The atomics
257 // can always be accessed using std::memory_order_relaxed since each value is
258 // separately recorded in UMA and no other memory accesses depend on it. Some
259 // values are correlated (eg. `address_cache_hits_` and
260 // `address_cache_misses_`), and this might see a write to one but not the
261 // other, but this shouldn't cause enough errors in the aggregated UMA metrics
262 // to be worth adding overhead to avoid it.
263 std::atomic<size_t> address_cache_hits_;
264 std::atomic<size_t> address_cache_misses_;
265 size_t address_cache_max_size_ GUARDED_BY(mutex_) = 0;
Joe Mason74ce1242025-03-24 16:48:32266 // The max load factor that's observed in sampled_addresses_set().
Joe Masonbc586dc2024-12-11 18:28:52267 float address_cache_max_load_factor_ GUARDED_BY(mutex_) = 0;
268
Joe Mason74ce1242025-03-24 16:48:32269 // The load factor that will trigger rebalancing in sampled_addresses_set().
270 // By definition `address_cache_max_load_factor_` will never exceed this.
271 float address_cache_target_load_factor_ GUARDED_BY(mutex_) = 1.0;
272
Alexei Filippovd6363e472018-08-27 19:31:39273 friend class NoDestructor<PoissonAllocationSampler>;
Joe Mason0119d492023-01-19 06:31:49274 friend class PoissonAllocationSamplerStateTest;
Alexei Filippov678cb252018-09-18 01:11:15275 friend class SamplingHeapProfilerTest;
Joe Masonea96b4e2022-04-26 16:07:42276 FRIEND_TEST_ALL_PREFIXES(PoissonAllocationSamplerTest, MuteHooksWithoutInit);
Joe Mason74ce1242025-03-24 16:48:32277 FRIEND_TEST_ALL_PREFIXES(PoissonAllocationSamplerLoadFactorTest,
278 BalanceSampledAddressesSet);
Joe Masond9f877d2021-11-30 20:27:28279 FRIEND_TEST_ALL_PREFIXES(SamplingHeapProfilerTest, HookedAllocatorMuted);
Alexei Filippovd6363e472018-08-27 19:31:39280};
281
André Kempe543fe172023-04-05 19:40:05282ALWAYS_INLINE void PoissonAllocationSampler::OnAllocation(
André Kempe69aec282024-01-03 19:28:45283 const base::allocator::dispatcher::AllocationNotificationData&
284 allocation_data) {
André Kempef3959c072023-01-19 19:49:29285 // The allocation hooks may be installed before the sampler is started. Check
286 // if its ever been started first to avoid extra work on the fast path,
287 // because it's the most common case.
288 const ProfilingStateFlagMask state =
289 profiling_state_.load(std::memory_order_relaxed);
Peter Kastingfa488992024-08-06 07:48:14290 if (!(state & ProfilingStateFlag::kWasStarted)) [[likely]] {
André Kempef3959c072023-01-19 19:49:29291 return;
292 }
293
André Kempe69aec282024-01-03 19:28:45294 const auto type = allocation_data.allocation_subsystem();
295
André Kempef3959c072023-01-19 19:49:29296 // When sampling is muted for testing, only handle manual calls to
297 // RecordAlloc. (This doesn't need to be checked in RecordFree because muted
298 // allocations won't be added to sampled_addresses_set(), so RecordFree
299 // already skips them.)
Peter Kastingfa488992024-08-06 07:48:14300 if ((state & ProfilingStateFlag::kHookedSamplesMutedForTesting) &&
301 type !=
302 base::allocator::dispatcher::AllocationSubsystem::kManualForTesting)
303 [[unlikely]] {
André Kempef3959c072023-01-19 19:49:29304 return;
305 }
306
André Kempe1f9ea582023-01-25 14:35:03307 // Note: ReentryGuard prevents from recursions introduced by malloc and
308 // initialization of thread local storage which happen in the allocation path
309 // only (please see docs of ReentryGuard for full details).
310 allocator::dispatcher::ReentryGuard reentry_guard;
311
Peter Kastingfa488992024-08-06 07:48:14312 if (!reentry_guard) [[unlikely]] {
André Kempe1f9ea582023-01-25 14:35:03313 return;
314 }
315
André Kempe69aec282024-01-03 19:28:45316 DoRecordAllocation(state, allocation_data.address(), allocation_data.size(),
317 type, allocation_data.type_name());
André Kempef3959c072023-01-19 19:49:29318}
319
André Kempe69aec282024-01-03 19:28:45320ALWAYS_INLINE void PoissonAllocationSampler::OnFree(
321 const base::allocator::dispatcher::FreeNotificationData& free_data) {
Joe Mason0119d492023-01-19 06:31:49322 // The allocation hooks may be installed before the sampler is started. Check
323 // if its ever been started first to avoid extra work on the fast path,
324 // because it's the most common case. Note that DoRecordFree still needs to be
325 // called if the sampler was started but is now stopped, to track allocations
326 // that were recorded while the sampler was still running.
327 //
328 // Relaxed ordering is safe here because there's only one case where
329 // RecordAlloc and RecordFree MUST see the same value of `profiling_state_`.
330 // Assume thread A updates `profiling_state_` from 0 to kWasStarted |
331 // kIsRunning, thread B calls RecordAlloc, and thread C calls RecordFree.
332 // (Something else could update `profiling_state_` to remove kIsRunning before
333 // RecordAlloc or RecordFree.)
334 //
335 // 1. If RecordAlloc(p) sees !kWasStarted or !kIsRunning it will return
336 // immediately, so p won't be in sampled_address_set(). So no matter what
337 // RecordFree(p) sees it will also return immediately.
338 //
339 // 2. If RecordFree() is called with a pointer that was never passed to
340 // RecordAlloc(), again it will return immediately no matter what it sees.
341 //
342 // 3. If RecordAlloc(p) sees kIsRunning it will put p in
343 // sampled_address_set(). In this case RecordFree(p) MUST see !kWasStarted
344 // or it will return without removing p:
345 //
346 // 3a. If the program got p as the return value from malloc() and passed it
347 // to free(), then RecordFree() happens-after RecordAlloc() and
348 // therefore will see the same value of `profiling_state_` as
349 // RecordAlloc() for all memory orders. (Proof: using the definitions
350 // of sequenced-after, happens-after and inter-thread happens-after
351 // from https://en.cppreference.com/w/cpp/atomic/memory_order, malloc()
352 // calls RecordAlloc() so its return is sequenced-after RecordAlloc();
353 // free() inter-thread happens-after malloc's return because it
354 // consumes the result; RecordFree() is sequenced-after its caller,
355 // free(); therefore RecordFree() interthread happens-after
356 // RecordAlloc().)
357 // 3b. If the program is freeing a random pointer which coincidentally was
358 // also returned from malloc(), such that free(p) does not happen-after
359 // malloc(), then there is already an unavoidable race condition. If
360 // the profiler sees malloc() before free(p), then it will add p to
361 // sampled_addresses_set() and then remove it; otherwise it will do
362 // nothing in RecordFree() and add p to sampled_addresses_set() in
363 // RecordAlloc(), recording a potential leak. Reading
364 // `profiling_state_` with relaxed ordering adds another possibility:
365 // if the profiler sees malloc() with kWasStarted and then free without
366 // kWasStarted, it will add p to sampled_addresses_set() in
367 // RecordAlloc() and then do nothing in RecordFree(). This has the same
368 // outcome as the existing race.
369 const ProfilingStateFlagMask state =
370 profiling_state_.load(std::memory_order_relaxed);
Peter Kastingfa488992024-08-06 07:48:14371 if (!(state & ProfilingStateFlag::kWasStarted)) [[likely]] {
Alexei Filippov02435742019-06-05 04:43:01372 return;
Joe Mason0119d492023-01-19 06:31:49373 }
André Kempe69aec282024-01-03 19:28:45374
375 void* const address = free_data.address();
376
Peter Kastingfa488992024-08-06 07:48:14377 if (address == nullptr) [[unlikely]] {
Joe Mason0119d492023-01-19 06:31:49378 return;
379 }
Peter Kastingfa488992024-08-06 07:48:14380 if (!sampled_addresses_set().Contains(address)) [[likely]] {
Joe Masonbc586dc2024-12-11 18:28:52381 address_cache_misses_.fetch_add(1, std::memory_order_relaxed);
André Kempef3959c072023-01-19 19:49:29382 return;
Joe Mason0119d492023-01-19 06:31:49383 }
Joe Masonbc586dc2024-12-11 18:28:52384 address_cache_hits_.fetch_add(1, std::memory_order_relaxed);
Peter Kastingfa488992024-08-06 07:48:14385 if (ScopedMuteThreadSamples::IsMuted()) [[unlikely]] {
André Kempef3959c072023-01-19 19:49:29386 return;
387 }
388
André Kempe1f9ea582023-01-25 14:35:03389 // Note: ReentryGuard prevents from recursions introduced by malloc and
390 // initialization of thread local storage which happen in the allocation path
391 // only (please see docs of ReentryGuard for full details). Therefore, the
392 // DoNotifyFree doesn't need to be guarded.
393
André Kempef3959c072023-01-19 19:49:29394 DoRecordFree(address);
Alexei Filippov02435742019-06-05 04:43:01395}
396
Alexei Filippovd6363e472018-08-27 19:31:39397} // namespace base
398
399#endif // BASE_SAMPLING_HEAP_PROFILER_POISSON_ALLOCATION_SAMPLER_H_