Avi Drissman | e4622aa | 2022-09-08 20:36:06 | [diff] [blame] | 1 | // Copyright 2018 The Chromium Authors |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 2 | // 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 Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 8 | #include <atomic> |
Joe Mason | 74ce124 | 2025-03-24 16:48:32 | [diff] [blame^] | 9 | #include <optional> |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 10 | #include <vector> |
| 11 | |
André Kempe | 69aec28 | 2024-01-03 19:28:45 | [diff] [blame] | 12 | #include "base/allocator/dispatcher/notification_data.h" |
André Kempe | 1f9ea58 | 2023-01-25 14:35:03 | [diff] [blame] | 13 | #include "base/allocator/dispatcher/reentry_guard.h" |
André Kempe | b99648e | 2023-01-06 17:21:20 | [diff] [blame] | 14 | #include "base/allocator/dispatcher/subsystem.h" |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 15 | #include "base/base_export.h" |
Alexei Filippov | 0243574 | 2019-06-05 04:43:01 | [diff] [blame] | 16 | #include "base/compiler_specific.h" |
Joe Mason | d9f877d | 2021-11-30 20:27:28 | [diff] [blame] | 17 | #include "base/gtest_prod_util.h" |
Arthur Sonzogni | b8e46e9 | 2024-05-31 08:01:19 | [diff] [blame] | 18 | #include "base/memory/raw_ptr_exclusion.h" |
Avi Drissman | ded7717 | 2021-07-02 18:23:00 | [diff] [blame] | 19 | #include "base/no_destructor.h" |
Alexei Filippov | 0243574 | 2019-06-05 04:43:01 | [diff] [blame] | 20 | #include "base/sampling_heap_profiler/lock_free_address_hash_set.h" |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 21 | #include "base/synchronization/lock.h" |
Alexei Filippov | f071fd4 | 2019-09-11 21:37:34 | [diff] [blame] | 22 | #include "base/thread_annotations.h" |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 23 | |
| 24 | namespace base { |
| 25 | |
Joe Mason | d9f877d | 2021-11-30 20:27:28 | [diff] [blame] | 26 | class SamplingHeapProfilerTest; |
| 27 | |
Joe Mason | bc586dc | 2024-12-11 18:28:52 | [diff] [blame] | 28 | // Stats about the allocation sampler. |
| 29 | struct BASE_EXPORT PoissonAllocationSamplerStats { |
Joe Mason | cf0e2e1 | 2024-12-11 22:09:47 | [diff] [blame] | 30 | PoissonAllocationSamplerStats( |
| 31 | size_t address_cache_hits, |
| 32 | size_t address_cache_misses, |
| 33 | size_t address_cache_max_size, |
| 34 | float address_cache_max_load_factor, |
| 35 | std::vector<size_t> address_cache_bucket_lengths); |
| 36 | ~PoissonAllocationSamplerStats(); |
| 37 | |
| 38 | PoissonAllocationSamplerStats(const PoissonAllocationSamplerStats&); |
| 39 | PoissonAllocationSamplerStats& operator=( |
| 40 | const PoissonAllocationSamplerStats&); |
| 41 | |
| 42 | size_t address_cache_hits; |
| 43 | size_t address_cache_misses; |
| 44 | size_t address_cache_max_size; |
| 45 | float address_cache_max_load_factor; |
| 46 | std::vector<size_t> address_cache_bucket_lengths; |
Joe Mason | bc586dc | 2024-12-11 18:28:52 | [diff] [blame] | 47 | }; |
| 48 | |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 49 | // This singleton class implements Poisson sampling of the incoming allocations |
| 50 | // stream. It hooks onto base::allocator and base::PartitionAlloc. |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 51 | // The only control parameter is sampling interval that controls average value |
| 52 | // of the sampling intervals. The actual intervals between samples are |
| 53 | // randomized using Poisson distribution to mitigate patterns in the allocation |
| 54 | // stream. |
| 55 | // Once accumulated allocation sizes fill up the current sample interval, |
| 56 | // a sample is generated and sent to the observers via |SampleAdded| call. |
| 57 | // When the corresponding memory that triggered the sample is freed observers |
| 58 | // get notified with |SampleRemoved| call. |
| 59 | // |
| 60 | class BASE_EXPORT PoissonAllocationSampler { |
| 61 | public: |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 62 | class SamplesObserver { |
| 63 | public: |
| 64 | virtual ~SamplesObserver() = default; |
André Kempe | b99648e | 2023-01-06 17:21:20 | [diff] [blame] | 65 | virtual void SampleAdded( |
| 66 | void* address, |
| 67 | size_t size, |
| 68 | size_t total, |
| 69 | base::allocator::dispatcher::AllocationSubsystem type, |
| 70 | const char* context) = 0; |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 71 | virtual void SampleRemoved(void* address) = 0; |
| 72 | }; |
| 73 | |
Joe Mason | d9f877d | 2021-11-30 20:27:28 | [diff] [blame] | 74 | // An instance of this class makes the sampler not report samples generated |
Alexei Filippov | 28cc68d | 2018-09-13 07:58:36 | [diff] [blame] | 75 | // within the object scope for the current thread. |
| 76 | // It allows observers to allocate/deallocate memory while holding a lock |
| 77 | // without a chance to get into reentrancy problems. |
Alexei Filippov | bc37963 | 2018-09-14 22:29:06 | [diff] [blame] | 78 | class BASE_EXPORT ScopedMuteThreadSamples { |
Alexei Filippov | 28cc68d | 2018-09-13 07:58:36 | [diff] [blame] | 79 | public: |
Alexei Filippov | bc37963 | 2018-09-14 22:29:06 | [diff] [blame] | 80 | ScopedMuteThreadSamples(); |
| 81 | ~ScopedMuteThreadSamples(); |
Alexei Filippov | e48985e | 2019-02-01 00:27:41 | [diff] [blame] | 82 | |
Joe Mason | d9f877d | 2021-11-30 20:27:28 | [diff] [blame] | 83 | ScopedMuteThreadSamples(const ScopedMuteThreadSamples&) = delete; |
| 84 | ScopedMuteThreadSamples& operator=(const ScopedMuteThreadSamples&) = delete; |
| 85 | |
Alexei Filippov | e48985e | 2019-02-01 00:27:41 | [diff] [blame] | 86 | static bool IsMuted(); |
Joe Mason | bb955419 | 2025-01-09 19:21:00 | [diff] [blame] | 87 | |
| 88 | private: |
| 89 | bool was_muted_ = false; |
Alexei Filippov | 28cc68d | 2018-09-13 07:58:36 | [diff] [blame] | 90 | }; |
| 91 | |
Joe Mason | f18dd4a | 2022-05-02 18:41:51 | [diff] [blame] | 92 | // An instance of this class makes the sampler behave deterministically to |
| 93 | // ensure test results are repeatable. Does not support nesting. |
| 94 | class BASE_EXPORT ScopedSuppressRandomnessForTesting { |
| 95 | public: |
| 96 | ScopedSuppressRandomnessForTesting(); |
| 97 | ~ScopedSuppressRandomnessForTesting(); |
| 98 | |
| 99 | ScopedSuppressRandomnessForTesting( |
| 100 | const ScopedSuppressRandomnessForTesting&) = delete; |
| 101 | ScopedSuppressRandomnessForTesting& operator=( |
| 102 | const ScopedSuppressRandomnessForTesting&) = delete; |
| 103 | |
| 104 | static bool IsSuppressed(); |
| 105 | }; |
| 106 | |
Joe Mason | 5fb082b4 | 2024-05-14 18:34:35 | [diff] [blame] | 107 | // An instance of this class makes the sampler only report samples with |
| 108 | // AllocatorType kManualForTesting, not those from hooked allocators. This |
| 109 | // allows unit tests to set test expectations based on only explicit calls to |
| 110 | // RecordAlloc and RecordFree. |
| 111 | // |
| 112 | // The accumulated bytes on the thread that creates a |
| 113 | // ScopedMuteHookedSamplesForTesting will also be reset to 0, and restored |
| 114 | // when the object leaves scope. This gives tests a known state to start |
| 115 | // recording samples on one thread: a full interval must pass to record a |
| 116 | // sample. Other threads will still have a random number of accumulated bytes. |
| 117 | // |
| 118 | // Only one instance may exist at a time. |
| 119 | class BASE_EXPORT ScopedMuteHookedSamplesForTesting { |
| 120 | public: |
| 121 | ScopedMuteHookedSamplesForTesting(); |
| 122 | ~ScopedMuteHookedSamplesForTesting(); |
| 123 | |
| 124 | // Move-only. |
| 125 | ScopedMuteHookedSamplesForTesting( |
| 126 | const ScopedMuteHookedSamplesForTesting&) = delete; |
| 127 | ScopedMuteHookedSamplesForTesting& operator=( |
| 128 | const ScopedMuteHookedSamplesForTesting&) = delete; |
| 129 | ScopedMuteHookedSamplesForTesting(ScopedMuteHookedSamplesForTesting&&); |
| 130 | ScopedMuteHookedSamplesForTesting& operator=( |
| 131 | ScopedMuteHookedSamplesForTesting&&); |
| 132 | |
| 133 | private: |
| 134 | intptr_t accumulated_bytes_snapshot_; |
| 135 | }; |
| 136 | |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 137 | // Must be called early during the process initialization. It creates and |
| 138 | // reserves a TLS slot. |
| 139 | static void Init(); |
| 140 | |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 141 | void AddSamplesObserver(SamplesObserver*); |
Alexei Filippov | f071fd4 | 2019-09-11 21:37:34 | [diff] [blame] | 142 | |
| 143 | // Note: After an observer is removed it is still possible to receive |
| 144 | // a notification to that observer. This is not a problem currently as |
| 145 | // the only client of this interface is the base::SamplingHeapProfiler, |
| 146 | // which is a singleton. |
| 147 | // If there's a need for this functionality in the future, one might |
| 148 | // want to put observers notification loop under a reader-writer lock. |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 149 | void RemoveSamplesObserver(SamplesObserver*); |
| 150 | |
Joe Mason | af0833ca | 2022-01-28 21:49:48 | [diff] [blame] | 151 | // Sets the mean number of bytes that will be allocated before taking a |
| 152 | // sample. |
| 153 | void SetSamplingInterval(size_t sampling_interval_bytes); |
| 154 | |
| 155 | // Returns the current mean sampling interval, in bytes. |
| 156 | size_t SamplingInterval() const; |
| 157 | |
Joe Mason | 74ce124 | 2025-03-24 16:48:32 | [diff] [blame^] | 158 | // Sets the max load factor before rebalancing the LockFreeAddressHashSet, or |
| 159 | // resets it to the default if `load_factor` is nulloptr. |
| 160 | void SetTargetHashSetLoadFactor(std::optional<float> load_factor); |
| 161 | |
Joe Mason | bc586dc | 2024-12-11 18:28:52 | [diff] [blame] | 162 | // Returns statistics about the allocation sampler, and resets the running |
| 163 | // counts so that each call to this returns only stats about the period |
| 164 | // between calls. |
| 165 | PoissonAllocationSamplerStats GetAndResetStats(); |
| 166 | |
André Kempe | 543fe17 | 2023-04-05 19:40:05 | [diff] [blame] | 167 | ALWAYS_INLINE void OnAllocation( |
André Kempe | 69aec28 | 2024-01-03 19:28:45 | [diff] [blame] | 168 | const base::allocator::dispatcher::AllocationNotificationData& |
| 169 | allocation_data); |
| 170 | ALWAYS_INLINE void OnFree( |
| 171 | const base::allocator::dispatcher::FreeNotificationData& free_data); |
André Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 172 | |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 173 | static PoissonAllocationSampler* Get(); |
| 174 | |
Peter Boström | 75cd3c0 | 2021-09-28 15:23:18 | [diff] [blame] | 175 | PoissonAllocationSampler(const PoissonAllocationSampler&) = delete; |
| 176 | PoissonAllocationSampler& operator=(const PoissonAllocationSampler&) = delete; |
| 177 | |
Joe Mason | 5fb082b4 | 2024-05-14 18:34:35 | [diff] [blame] | 178 | // Returns true if a ScopedMuteHookedSamplesForTesting exists. This can be |
| 179 | // read from any thread. |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 180 | static bool AreHookedSamplesMuted() { |
| 181 | return profiling_state_.load(std::memory_order_relaxed) & |
| 182 | ProfilingStateFlag::kHookedSamplesMutedForTesting; |
| 183 | } |
Joe Mason | d9f877d | 2021-11-30 20:27:28 | [diff] [blame] | 184 | |
Joe Mason | bb955419 | 2025-01-09 19:21:00 | [diff] [blame] | 185 | // Returns the number of allocated bytes that have been observed. |
| 186 | static intptr_t GetAccumulatedBytesForTesting(); |
| 187 | |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 188 | private: |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 189 | // Flags recording the state of the profiler. This does not use enum class so |
| 190 | // flags can be used in a bitmask. |
| 191 | enum ProfilingStateFlag { |
| 192 | // Set if profiling has ever been started in this session of Chrome. Once |
| 193 | // this is set, it is never reset. This is used to optimize the common case |
| 194 | // where profiling is never used. |
| 195 | kWasStarted = 1 << 0, |
| 196 | // Set if profiling is currently running. This flag is toggled on and off |
| 197 | // as sample observers are added and removed. |
| 198 | kIsRunning = 1 << 1, |
| 199 | // Set if a ScopedMuteHookedSamplesForTesting object exists. |
| 200 | kHookedSamplesMutedForTesting = 1 << 2, |
| 201 | }; |
| 202 | using ProfilingStateFlagMask = int; |
| 203 | |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 204 | PoissonAllocationSampler(); |
| 205 | ~PoissonAllocationSampler() = delete; |
| 206 | |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 207 | static size_t GetNextSampleInterval(size_t base_interval); |
Joe Mason | ea96b4e | 2022-04-26 16:07:42 | [diff] [blame] | 208 | |
| 209 | // Return the set of sampled addresses. This is only valid to call after |
| 210 | // Init(). |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 211 | static LockFreeAddressHashSet& sampled_addresses_set(); |
| 212 | |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 213 | // Atomically adds `flag` to `profiling_state_`. DCHECK's if it was already |
| 214 | // set. If `flag` is kIsRunning, also sets kWasStarted. Uses |
| 215 | // std::memory_order_relaxed semantics and therefore doesn't synchronize the |
| 216 | // state of any other memory with future readers. (See the comment in |
| 217 | // RecordFree() for why this is safe.) |
| 218 | static void SetProfilingStateFlag(ProfilingStateFlag flag); |
| 219 | |
| 220 | // Atomically removes `flag` from `profiling_state_`. DCHECK's if it was not |
| 221 | // already set. Uses std::memory_order_relaxed semantics and therefore doesn't |
| 222 | // synchronize the state of any other memory with future readers. (See the |
| 223 | // comment in RecordFree() for why this is safe.) |
| 224 | static void ResetProfilingStateFlag(ProfilingStateFlag flag); |
| 225 | |
André Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 226 | void DoRecordAllocation(const ProfilingStateFlagMask state, |
| 227 | void* address, |
| 228 | size_t size, |
| 229 | base::allocator::dispatcher::AllocationSubsystem type, |
| 230 | const char* context); |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 231 | void DoRecordFree(void* address); |
| 232 | |
Joe Mason | 74ce124 | 2025-03-24 16:48:32 | [diff] [blame^] | 233 | void BalanceAddressesHashSet() EXCLUSIVE_LOCKS_REQUIRED(mutex_); |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 234 | |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 235 | Lock mutex_; |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 236 | |
Alexei Filippov | f071fd4 | 2019-09-11 21:37:34 | [diff] [blame] | 237 | // The |observers_| list is guarded by |mutex_|, however a copy of it |
| 238 | // is made before invoking the observers (to avoid performing expensive |
| 239 | // operations under the lock) as such the SamplesObservers themselves need |
| 240 | // to be thread-safe and support being invoked racily after |
| 241 | // RemoveSamplesObserver(). |
Arthur Sonzogni | b8e46e9 | 2024-05-31 08:01:19 | [diff] [blame] | 242 | // |
| 243 | // This class handles allocation, so it must never use raw_ptr<T>. In |
| 244 | // particular, raw_ptr<T> with `enable_backup_ref_ptr_instance_tracer` |
| 245 | // developer option allocates memory, which would cause reentrancy issues: |
| 246 | // allocating memory while allocating memory. |
| 247 | // More details in https://crbug.com/340815319 |
| 248 | RAW_PTR_EXCLUSION std::vector<SamplesObserver*> observers_ GUARDED_BY(mutex_); |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 249 | |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 250 | // Fast, thread-safe access to the current profiling state. |
| 251 | static std::atomic<ProfilingStateFlagMask> profiling_state_; |
| 252 | |
Joe Mason | bc586dc | 2024-12-11 18:28:52 | [diff] [blame] | 253 | // Running counts for PoissonAllocationSamplerStats. These are all atomic or |
| 254 | // mutex-guarded because they're updated from multiple threads. The atomics |
| 255 | // can always be accessed using std::memory_order_relaxed since each value is |
| 256 | // separately recorded in UMA and no other memory accesses depend on it. Some |
| 257 | // values are correlated (eg. `address_cache_hits_` and |
| 258 | // `address_cache_misses_`), and this might see a write to one but not the |
| 259 | // other, but this shouldn't cause enough errors in the aggregated UMA metrics |
| 260 | // to be worth adding overhead to avoid it. |
| 261 | std::atomic<size_t> address_cache_hits_; |
| 262 | std::atomic<size_t> address_cache_misses_; |
| 263 | size_t address_cache_max_size_ GUARDED_BY(mutex_) = 0; |
Joe Mason | 74ce124 | 2025-03-24 16:48:32 | [diff] [blame^] | 264 | // The max load factor that's observed in sampled_addresses_set(). |
Joe Mason | bc586dc | 2024-12-11 18:28:52 | [diff] [blame] | 265 | float address_cache_max_load_factor_ GUARDED_BY(mutex_) = 0; |
| 266 | |
Joe Mason | 74ce124 | 2025-03-24 16:48:32 | [diff] [blame^] | 267 | // The load factor that will trigger rebalancing in sampled_addresses_set(). |
| 268 | // By definition `address_cache_max_load_factor_` will never exceed this. |
| 269 | float address_cache_target_load_factor_ GUARDED_BY(mutex_) = 1.0; |
| 270 | |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 271 | friend class NoDestructor<PoissonAllocationSampler>; |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 272 | friend class PoissonAllocationSamplerStateTest; |
Alexei Filippov | 678cb25 | 2018-09-18 01:11:15 | [diff] [blame] | 273 | friend class SamplingHeapProfilerTest; |
Joe Mason | ea96b4e | 2022-04-26 16:07:42 | [diff] [blame] | 274 | FRIEND_TEST_ALL_PREFIXES(PoissonAllocationSamplerTest, MuteHooksWithoutInit); |
Joe Mason | 74ce124 | 2025-03-24 16:48:32 | [diff] [blame^] | 275 | FRIEND_TEST_ALL_PREFIXES(PoissonAllocationSamplerLoadFactorTest, |
| 276 | BalanceSampledAddressesSet); |
Joe Mason | d9f877d | 2021-11-30 20:27:28 | [diff] [blame] | 277 | FRIEND_TEST_ALL_PREFIXES(SamplingHeapProfilerTest, HookedAllocatorMuted); |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 278 | }; |
| 279 | |
André Kempe | 543fe17 | 2023-04-05 19:40:05 | [diff] [blame] | 280 | ALWAYS_INLINE void PoissonAllocationSampler::OnAllocation( |
André Kempe | 69aec28 | 2024-01-03 19:28:45 | [diff] [blame] | 281 | const base::allocator::dispatcher::AllocationNotificationData& |
| 282 | allocation_data) { |
André Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 283 | // The allocation hooks may be installed before the sampler is started. Check |
| 284 | // if its ever been started first to avoid extra work on the fast path, |
| 285 | // because it's the most common case. |
| 286 | const ProfilingStateFlagMask state = |
| 287 | profiling_state_.load(std::memory_order_relaxed); |
Peter Kasting | fa48899 | 2024-08-06 07:48:14 | [diff] [blame] | 288 | if (!(state & ProfilingStateFlag::kWasStarted)) [[likely]] { |
André Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 289 | return; |
| 290 | } |
| 291 | |
André Kempe | 69aec28 | 2024-01-03 19:28:45 | [diff] [blame] | 292 | const auto type = allocation_data.allocation_subsystem(); |
| 293 | |
André Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 294 | // When sampling is muted for testing, only handle manual calls to |
| 295 | // RecordAlloc. (This doesn't need to be checked in RecordFree because muted |
| 296 | // allocations won't be added to sampled_addresses_set(), so RecordFree |
| 297 | // already skips them.) |
Peter Kasting | fa48899 | 2024-08-06 07:48:14 | [diff] [blame] | 298 | if ((state & ProfilingStateFlag::kHookedSamplesMutedForTesting) && |
| 299 | type != |
| 300 | base::allocator::dispatcher::AllocationSubsystem::kManualForTesting) |
| 301 | [[unlikely]] { |
André Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 302 | return; |
| 303 | } |
| 304 | |
André Kempe | 1f9ea58 | 2023-01-25 14:35:03 | [diff] [blame] | 305 | // Note: ReentryGuard prevents from recursions introduced by malloc and |
| 306 | // initialization of thread local storage which happen in the allocation path |
| 307 | // only (please see docs of ReentryGuard for full details). |
| 308 | allocator::dispatcher::ReentryGuard reentry_guard; |
| 309 | |
Peter Kasting | fa48899 | 2024-08-06 07:48:14 | [diff] [blame] | 310 | if (!reentry_guard) [[unlikely]] { |
André Kempe | 1f9ea58 | 2023-01-25 14:35:03 | [diff] [blame] | 311 | return; |
| 312 | } |
| 313 | |
André Kempe | 69aec28 | 2024-01-03 19:28:45 | [diff] [blame] | 314 | DoRecordAllocation(state, allocation_data.address(), allocation_data.size(), |
| 315 | type, allocation_data.type_name()); |
André Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 316 | } |
| 317 | |
André Kempe | 69aec28 | 2024-01-03 19:28:45 | [diff] [blame] | 318 | ALWAYS_INLINE void PoissonAllocationSampler::OnFree( |
| 319 | const base::allocator::dispatcher::FreeNotificationData& free_data) { |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 320 | // The allocation hooks may be installed before the sampler is started. Check |
| 321 | // if its ever been started first to avoid extra work on the fast path, |
| 322 | // because it's the most common case. Note that DoRecordFree still needs to be |
| 323 | // called if the sampler was started but is now stopped, to track allocations |
| 324 | // that were recorded while the sampler was still running. |
| 325 | // |
| 326 | // Relaxed ordering is safe here because there's only one case where |
| 327 | // RecordAlloc and RecordFree MUST see the same value of `profiling_state_`. |
| 328 | // Assume thread A updates `profiling_state_` from 0 to kWasStarted | |
| 329 | // kIsRunning, thread B calls RecordAlloc, and thread C calls RecordFree. |
| 330 | // (Something else could update `profiling_state_` to remove kIsRunning before |
| 331 | // RecordAlloc or RecordFree.) |
| 332 | // |
| 333 | // 1. If RecordAlloc(p) sees !kWasStarted or !kIsRunning it will return |
| 334 | // immediately, so p won't be in sampled_address_set(). So no matter what |
| 335 | // RecordFree(p) sees it will also return immediately. |
| 336 | // |
| 337 | // 2. If RecordFree() is called with a pointer that was never passed to |
| 338 | // RecordAlloc(), again it will return immediately no matter what it sees. |
| 339 | // |
| 340 | // 3. If RecordAlloc(p) sees kIsRunning it will put p in |
| 341 | // sampled_address_set(). In this case RecordFree(p) MUST see !kWasStarted |
| 342 | // or it will return without removing p: |
| 343 | // |
| 344 | // 3a. If the program got p as the return value from malloc() and passed it |
| 345 | // to free(), then RecordFree() happens-after RecordAlloc() and |
| 346 | // therefore will see the same value of `profiling_state_` as |
| 347 | // RecordAlloc() for all memory orders. (Proof: using the definitions |
| 348 | // of sequenced-after, happens-after and inter-thread happens-after |
| 349 | // from https://en.cppreference.com/w/cpp/atomic/memory_order, malloc() |
| 350 | // calls RecordAlloc() so its return is sequenced-after RecordAlloc(); |
| 351 | // free() inter-thread happens-after malloc's return because it |
| 352 | // consumes the result; RecordFree() is sequenced-after its caller, |
| 353 | // free(); therefore RecordFree() interthread happens-after |
| 354 | // RecordAlloc().) |
| 355 | // 3b. If the program is freeing a random pointer which coincidentally was |
| 356 | // also returned from malloc(), such that free(p) does not happen-after |
| 357 | // malloc(), then there is already an unavoidable race condition. If |
| 358 | // the profiler sees malloc() before free(p), then it will add p to |
| 359 | // sampled_addresses_set() and then remove it; otherwise it will do |
| 360 | // nothing in RecordFree() and add p to sampled_addresses_set() in |
| 361 | // RecordAlloc(), recording a potential leak. Reading |
| 362 | // `profiling_state_` with relaxed ordering adds another possibility: |
| 363 | // if the profiler sees malloc() with kWasStarted and then free without |
| 364 | // kWasStarted, it will add p to sampled_addresses_set() in |
| 365 | // RecordAlloc() and then do nothing in RecordFree(). This has the same |
| 366 | // outcome as the existing race. |
| 367 | const ProfilingStateFlagMask state = |
| 368 | profiling_state_.load(std::memory_order_relaxed); |
Peter Kasting | fa48899 | 2024-08-06 07:48:14 | [diff] [blame] | 369 | if (!(state & ProfilingStateFlag::kWasStarted)) [[likely]] { |
Alexei Filippov | 0243574 | 2019-06-05 04:43:01 | [diff] [blame] | 370 | return; |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 371 | } |
André Kempe | 69aec28 | 2024-01-03 19:28:45 | [diff] [blame] | 372 | |
| 373 | void* const address = free_data.address(); |
| 374 | |
Peter Kasting | fa48899 | 2024-08-06 07:48:14 | [diff] [blame] | 375 | if (address == nullptr) [[unlikely]] { |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 376 | return; |
| 377 | } |
Peter Kasting | fa48899 | 2024-08-06 07:48:14 | [diff] [blame] | 378 | if (!sampled_addresses_set().Contains(address)) [[likely]] { |
Joe Mason | bc586dc | 2024-12-11 18:28:52 | [diff] [blame] | 379 | address_cache_misses_.fetch_add(1, std::memory_order_relaxed); |
André Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 380 | return; |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 381 | } |
Joe Mason | bc586dc | 2024-12-11 18:28:52 | [diff] [blame] | 382 | address_cache_hits_.fetch_add(1, std::memory_order_relaxed); |
Peter Kasting | fa48899 | 2024-08-06 07:48:14 | [diff] [blame] | 383 | if (ScopedMuteThreadSamples::IsMuted()) [[unlikely]] { |
André Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 384 | return; |
| 385 | } |
| 386 | |
André Kempe | 1f9ea58 | 2023-01-25 14:35:03 | [diff] [blame] | 387 | // Note: ReentryGuard prevents from recursions introduced by malloc and |
| 388 | // initialization of thread local storage which happen in the allocation path |
| 389 | // only (please see docs of ReentryGuard for full details). Therefore, the |
| 390 | // DoNotifyFree doesn't need to be guarded. |
| 391 | |
André Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 392 | DoRecordFree(address); |
Alexei Filippov | 0243574 | 2019-06-05 04:43:01 | [diff] [blame] | 393 | } |
| 394 | |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 395 | } // namespace base |
| 396 | |
| 397 | #endif // BASE_SAMPLING_HEAP_PROFILER_POISSON_ALLOCATION_SAMPLER_H_ |