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 | df3cdbc | 2025-06-18 20:07:05 | [diff] [blame] | 30 | using AddressCacheBucketStats = LockFreeAddressHashSet::BucketStats; |
| 31 | |
Joe Mason | cf0e2e1 | 2024-12-11 22:09:47 | [diff] [blame] | 32 | 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 Mason | df3cdbc | 2025-06-18 20:07:05 | [diff] [blame] | 37 | AddressCacheBucketStats address_cache_bucket_stats); |
Joe Mason | cf0e2e1 | 2024-12-11 22:09:47 | [diff] [blame] | 38 | ~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 Mason | df3cdbc | 2025-06-18 20:07:05 | [diff] [blame] | 48 | AddressCacheBucketStats address_cache_bucket_stats; |
Joe Mason | bc586dc | 2024-12-11 18:28:52 | [diff] [blame] | 49 | }; |
| 50 | |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 51 | // This singleton class implements Poisson sampling of the incoming allocations |
| 52 | // stream. It hooks onto base::allocator and base::PartitionAlloc. |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 53 | // 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 | // |
| 62 | class BASE_EXPORT PoissonAllocationSampler { |
| 63 | public: |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 64 | class SamplesObserver { |
| 65 | public: |
| 66 | virtual ~SamplesObserver() = default; |
André Kempe | b99648e | 2023-01-06 17:21:20 | [diff] [blame] | 67 | 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 Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 73 | virtual void SampleRemoved(void* address) = 0; |
| 74 | }; |
| 75 | |
Joe Mason | d9f877d | 2021-11-30 20:27:28 | [diff] [blame] | 76 | // An instance of this class makes the sampler not report samples generated |
Alexei Filippov | 28cc68d | 2018-09-13 07:58:36 | [diff] [blame] | 77 | // 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 Filippov | bc37963 | 2018-09-14 22:29:06 | [diff] [blame] | 80 | class BASE_EXPORT ScopedMuteThreadSamples { |
Alexei Filippov | 28cc68d | 2018-09-13 07:58:36 | [diff] [blame] | 81 | public: |
Alexei Filippov | bc37963 | 2018-09-14 22:29:06 | [diff] [blame] | 82 | ScopedMuteThreadSamples(); |
| 83 | ~ScopedMuteThreadSamples(); |
Alexei Filippov | e48985e | 2019-02-01 00:27:41 | [diff] [blame] | 84 | |
Joe Mason | d9f877d | 2021-11-30 20:27:28 | [diff] [blame] | 85 | ScopedMuteThreadSamples(const ScopedMuteThreadSamples&) = delete; |
| 86 | ScopedMuteThreadSamples& operator=(const ScopedMuteThreadSamples&) = delete; |
| 87 | |
Alexei Filippov | e48985e | 2019-02-01 00:27:41 | [diff] [blame] | 88 | static bool IsMuted(); |
Joe Mason | bb955419 | 2025-01-09 19:21:00 | [diff] [blame] | 89 | |
| 90 | private: |
| 91 | bool was_muted_ = false; |
Alexei Filippov | 28cc68d | 2018-09-13 07:58:36 | [diff] [blame] | 92 | }; |
| 93 | |
Joe Mason | f18dd4a | 2022-05-02 18:41:51 | [diff] [blame] | 94 | // 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 Mason | 5fb082b4 | 2024-05-14 18:34:35 | [diff] [blame] | 109 | // 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 Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 139 | // Must be called early during the process initialization. It creates and |
| 140 | // reserves a TLS slot. |
| 141 | static void Init(); |
| 142 | |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 143 | void AddSamplesObserver(SamplesObserver*); |
Alexei Filippov | f071fd4 | 2019-09-11 21:37:34 | [diff] [blame] | 144 | |
| 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 Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 151 | void RemoveSamplesObserver(SamplesObserver*); |
| 152 | |
Joe Mason | af0833ca | 2022-01-28 21:49:48 | [diff] [blame] | 153 | // 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 Mason | 74ce124 | 2025-03-24 16:48:32 | [diff] [blame] | 160 | // 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 Mason | bc586dc | 2024-12-11 18:28:52 | [diff] [blame] | 164 | // 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é Kempe | 543fe17 | 2023-04-05 19:40:05 | [diff] [blame] | 169 | ALWAYS_INLINE void OnAllocation( |
André Kempe | 69aec28 | 2024-01-03 19:28:45 | [diff] [blame] | 170 | const base::allocator::dispatcher::AllocationNotificationData& |
| 171 | allocation_data); |
| 172 | ALWAYS_INLINE void OnFree( |
| 173 | const base::allocator::dispatcher::FreeNotificationData& free_data); |
André Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 174 | |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 175 | static PoissonAllocationSampler* Get(); |
| 176 | |
Peter Boström | 75cd3c0 | 2021-09-28 15:23:18 | [diff] [blame] | 177 | PoissonAllocationSampler(const PoissonAllocationSampler&) = delete; |
| 178 | PoissonAllocationSampler& operator=(const PoissonAllocationSampler&) = delete; |
| 179 | |
Joe Mason | 5fb082b4 | 2024-05-14 18:34:35 | [diff] [blame] | 180 | // Returns true if a ScopedMuteHookedSamplesForTesting exists. This can be |
| 181 | // read from any thread. |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 182 | static bool AreHookedSamplesMuted() { |
| 183 | return profiling_state_.load(std::memory_order_relaxed) & |
| 184 | ProfilingStateFlag::kHookedSamplesMutedForTesting; |
| 185 | } |
Joe Mason | d9f877d | 2021-11-30 20:27:28 | [diff] [blame] | 186 | |
Joe Mason | bb955419 | 2025-01-09 19:21:00 | [diff] [blame] | 187 | // Returns the number of allocated bytes that have been observed. |
| 188 | static intptr_t GetAccumulatedBytesForTesting(); |
| 189 | |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 190 | private: |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 191 | // 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 Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 206 | PoissonAllocationSampler(); |
| 207 | ~PoissonAllocationSampler() = delete; |
| 208 | |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 209 | static size_t GetNextSampleInterval(size_t base_interval); |
Joe Mason | ea96b4e | 2022-04-26 16:07:42 | [diff] [blame] | 210 | |
| 211 | // Return the set of sampled addresses. This is only valid to call after |
| 212 | // Init(). |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 213 | static LockFreeAddressHashSet& sampled_addresses_set(); |
| 214 | |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 215 | // 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é Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 228 | void DoRecordAllocation(const ProfilingStateFlagMask state, |
| 229 | void* address, |
| 230 | size_t size, |
| 231 | base::allocator::dispatcher::AllocationSubsystem type, |
| 232 | const char* context); |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 233 | void DoRecordFree(void* address); |
| 234 | |
Joe Mason | 74ce124 | 2025-03-24 16:48:32 | [diff] [blame] | 235 | void BalanceAddressesHashSet() EXCLUSIVE_LOCKS_REQUIRED(mutex_); |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 236 | |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 237 | Lock mutex_; |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 238 | |
Alexei Filippov | f071fd4 | 2019-09-11 21:37:34 | [diff] [blame] | 239 | // 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 Sonzogni | b8e46e9 | 2024-05-31 08:01:19 | [diff] [blame] | 244 | // |
| 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 Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 251 | |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 252 | // Fast, thread-safe access to the current profiling state. |
| 253 | static std::atomic<ProfilingStateFlagMask> profiling_state_; |
| 254 | |
Joe Mason | bc586dc | 2024-12-11 18:28:52 | [diff] [blame] | 255 | // 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 Mason | 74ce124 | 2025-03-24 16:48:32 | [diff] [blame] | 266 | // The max load factor that's observed in sampled_addresses_set(). |
Joe Mason | bc586dc | 2024-12-11 18:28:52 | [diff] [blame] | 267 | float address_cache_max_load_factor_ GUARDED_BY(mutex_) = 0; |
| 268 | |
Joe Mason | 74ce124 | 2025-03-24 16:48:32 | [diff] [blame] | 269 | // 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 Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 273 | friend class NoDestructor<PoissonAllocationSampler>; |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 274 | friend class PoissonAllocationSamplerStateTest; |
Alexei Filippov | 678cb25 | 2018-09-18 01:11:15 | [diff] [blame] | 275 | friend class SamplingHeapProfilerTest; |
Joe Mason | ea96b4e | 2022-04-26 16:07:42 | [diff] [blame] | 276 | FRIEND_TEST_ALL_PREFIXES(PoissonAllocationSamplerTest, MuteHooksWithoutInit); |
Joe Mason | 74ce124 | 2025-03-24 16:48:32 | [diff] [blame] | 277 | FRIEND_TEST_ALL_PREFIXES(PoissonAllocationSamplerLoadFactorTest, |
| 278 | BalanceSampledAddressesSet); |
Joe Mason | d9f877d | 2021-11-30 20:27:28 | [diff] [blame] | 279 | FRIEND_TEST_ALL_PREFIXES(SamplingHeapProfilerTest, HookedAllocatorMuted); |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 280 | }; |
| 281 | |
André Kempe | 543fe17 | 2023-04-05 19:40:05 | [diff] [blame] | 282 | ALWAYS_INLINE void PoissonAllocationSampler::OnAllocation( |
André Kempe | 69aec28 | 2024-01-03 19:28:45 | [diff] [blame] | 283 | const base::allocator::dispatcher::AllocationNotificationData& |
| 284 | allocation_data) { |
André Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 285 | // 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 Kasting | fa48899 | 2024-08-06 07:48:14 | [diff] [blame] | 290 | if (!(state & ProfilingStateFlag::kWasStarted)) [[likely]] { |
André Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 291 | return; |
| 292 | } |
| 293 | |
André Kempe | 69aec28 | 2024-01-03 19:28:45 | [diff] [blame] | 294 | const auto type = allocation_data.allocation_subsystem(); |
| 295 | |
André Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 296 | // 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 Kasting | fa48899 | 2024-08-06 07:48:14 | [diff] [blame] | 300 | if ((state & ProfilingStateFlag::kHookedSamplesMutedForTesting) && |
| 301 | type != |
| 302 | base::allocator::dispatcher::AllocationSubsystem::kManualForTesting) |
| 303 | [[unlikely]] { |
André Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 304 | return; |
| 305 | } |
| 306 | |
André Kempe | 1f9ea58 | 2023-01-25 14:35:03 | [diff] [blame] | 307 | // 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 Kasting | fa48899 | 2024-08-06 07:48:14 | [diff] [blame] | 312 | if (!reentry_guard) [[unlikely]] { |
André Kempe | 1f9ea58 | 2023-01-25 14:35:03 | [diff] [blame] | 313 | return; |
| 314 | } |
| 315 | |
André Kempe | 69aec28 | 2024-01-03 19:28:45 | [diff] [blame] | 316 | DoRecordAllocation(state, allocation_data.address(), allocation_data.size(), |
| 317 | type, allocation_data.type_name()); |
André Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 318 | } |
| 319 | |
André Kempe | 69aec28 | 2024-01-03 19:28:45 | [diff] [blame] | 320 | ALWAYS_INLINE void PoissonAllocationSampler::OnFree( |
| 321 | const base::allocator::dispatcher::FreeNotificationData& free_data) { |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 322 | // 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 Kasting | fa48899 | 2024-08-06 07:48:14 | [diff] [blame] | 371 | if (!(state & ProfilingStateFlag::kWasStarted)) [[likely]] { |
Alexei Filippov | 0243574 | 2019-06-05 04:43:01 | [diff] [blame] | 372 | return; |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 373 | } |
André Kempe | 69aec28 | 2024-01-03 19:28:45 | [diff] [blame] | 374 | |
| 375 | void* const address = free_data.address(); |
| 376 | |
Peter Kasting | fa48899 | 2024-08-06 07:48:14 | [diff] [blame] | 377 | if (address == nullptr) [[unlikely]] { |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 378 | return; |
| 379 | } |
Peter Kasting | fa48899 | 2024-08-06 07:48:14 | [diff] [blame] | 380 | if (!sampled_addresses_set().Contains(address)) [[likely]] { |
Joe Mason | bc586dc | 2024-12-11 18:28:52 | [diff] [blame] | 381 | address_cache_misses_.fetch_add(1, std::memory_order_relaxed); |
André Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 382 | return; |
Joe Mason | 0119d49 | 2023-01-19 06:31:49 | [diff] [blame] | 383 | } |
Joe Mason | bc586dc | 2024-12-11 18:28:52 | [diff] [blame] | 384 | address_cache_hits_.fetch_add(1, std::memory_order_relaxed); |
Peter Kasting | fa48899 | 2024-08-06 07:48:14 | [diff] [blame] | 385 | if (ScopedMuteThreadSamples::IsMuted()) [[unlikely]] { |
André Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 386 | return; |
| 387 | } |
| 388 | |
André Kempe | 1f9ea58 | 2023-01-25 14:35:03 | [diff] [blame] | 389 | // 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é Kempe | f3959c07 | 2023-01-19 19:49:29 | [diff] [blame] | 394 | DoRecordFree(address); |
Alexei Filippov | 0243574 | 2019-06-05 04:43:01 | [diff] [blame] | 395 | } |
| 396 | |
Alexei Filippov | d6363e47 | 2018-08-27 19:31:39 | [diff] [blame] | 397 | } // namespace base |
| 398 | |
| 399 | #endif // BASE_SAMPLING_HEAP_PROFILER_POISSON_ALLOCATION_SAMPLER_H_ |