1 | // Copyright 2018 The Abseil Authors. |
2 | // |
3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
4 | // you may not use this file except in compliance with the License. |
5 | // You may obtain a copy of the License at |
6 | // |
7 | // https://www.apache.org/licenses/LICENSE-2.0 |
8 | // |
9 | // Unless required by applicable law or agreed to in writing, software |
10 | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | // See the License for the specific language governing permissions and |
13 | // limitations under the License. |
14 | // |
15 | // ----------------------------------------------------------------------------- |
16 | // File: hashtablez_sampler.h |
17 | // ----------------------------------------------------------------------------- |
18 | // |
19 | // This header file defines the API for a low level library to sample hashtables |
20 | // and collect runtime statistics about them. |
21 | // |
22 | // `HashtablezSampler` controls the lifecycle of `HashtablezInfo` objects which |
23 | // store information about a single sample. |
24 | // |
25 | // `Record*` methods store information into samples. |
26 | // `Sample()` and `Unsample()` make use of a single global sampler with |
27 | // properties controlled by the flags hashtablez_enabled, |
28 | // hashtablez_sample_rate, and hashtablez_max_samples. |
29 | // |
30 | // WARNING |
31 | // |
32 | // Using this sampling API may cause sampled Swiss tables to use the global |
33 | // allocator (operator `new`) in addition to any custom allocator. If you |
34 | // are using a table in an unusual circumstance where allocation or calling a |
35 | // linux syscall is unacceptable, this could interfere. |
36 | // |
37 | // This utility is internal-only. Use at your own risk. |
38 | |
39 | #ifndef ABSL_CONTAINER_INTERNAL_HASHTABLEZ_SAMPLER_H_ |
40 | #define ABSL_CONTAINER_INTERNAL_HASHTABLEZ_SAMPLER_H_ |
41 | |
42 | #include <atomic> |
43 | #include <functional> |
44 | #include <memory> |
45 | #include <vector> |
46 | |
47 | #include "absl/base/internal/per_thread_tls.h" |
48 | #include "absl/base/optimization.h" |
49 | #include "absl/container/internal/have_sse.h" |
50 | #include "absl/synchronization/mutex.h" |
51 | #include "absl/utility/utility.h" |
52 | |
53 | namespace absl { |
54 | namespace container_internal { |
55 | |
56 | // Stores information about a sampled hashtable. All mutations to this *must* |
57 | // be made through `Record*` functions below. All reads from this *must* only |
58 | // occur in the callback to `HashtablezSampler::Iterate`. |
59 | struct HashtablezInfo { |
60 | // Constructs the object but does not fill in any fields. |
61 | HashtablezInfo(); |
62 | ~HashtablezInfo(); |
63 | HashtablezInfo(const HashtablezInfo&) = delete; |
64 | HashtablezInfo& operator=(const HashtablezInfo&) = delete; |
65 | |
66 | // Puts the object into a clean state, fills in the logically `const` members, |
67 | // blocking for any readers that are currently sampling the object. |
68 | void PrepareForSampling() EXCLUSIVE_LOCKS_REQUIRED(init_mu); |
69 | |
70 | // These fields are mutated by the various Record* APIs and need to be |
71 | // thread-safe. |
72 | std::atomic<size_t> capacity; |
73 | std::atomic<size_t> size; |
74 | std::atomic<size_t> num_erases; |
75 | std::atomic<size_t> max_probe_length; |
76 | std::atomic<size_t> total_probe_length; |
77 | std::atomic<size_t> hashes_bitwise_or; |
78 | std::atomic<size_t> hashes_bitwise_and; |
79 | |
80 | // `HashtablezSampler` maintains intrusive linked lists for all samples. See |
81 | // comments on `HashtablezSampler::all_` for details on these. `init_mu` |
82 | // guards the ability to restore the sample to a pristine state. This |
83 | // prevents races with sampling and resurrecting an object. |
84 | absl::Mutex init_mu; |
85 | HashtablezInfo* next; |
86 | HashtablezInfo* dead GUARDED_BY(init_mu); |
87 | |
88 | // All of the fields below are set by `PrepareForSampling`, they must not be |
89 | // mutated in `Record*` functions. They are logically `const` in that sense. |
90 | // These are guarded by init_mu, but that is not externalized to clients, who |
91 | // can only read them during `HashtablezSampler::Iterate` which will hold the |
92 | // lock. |
93 | static constexpr int kMaxStackDepth = 64; |
94 | absl::Time create_time; |
95 | int32_t depth; |
96 | void* stack[kMaxStackDepth]; |
97 | }; |
98 | |
99 | inline void RecordRehashSlow(HashtablezInfo* info, size_t total_probe_length) { |
100 | #if SWISSTABLE_HAVE_SSE2 |
101 | total_probe_length /= 16; |
102 | #else |
103 | total_probe_length /= 8; |
104 | #endif |
105 | info->total_probe_length.store(total_probe_length, std::memory_order_relaxed); |
106 | info->num_erases.store(0, std::memory_order_relaxed); |
107 | } |
108 | |
109 | inline void RecordStorageChangedSlow(HashtablezInfo* info, size_t size, |
110 | size_t capacity) { |
111 | info->size.store(size, std::memory_order_relaxed); |
112 | info->capacity.store(capacity, std::memory_order_relaxed); |
113 | if (size == 0) { |
114 | // This is a clear, reset the total/num_erases too. |
115 | RecordRehashSlow(info, 0); |
116 | } |
117 | } |
118 | |
119 | void RecordInsertSlow(HashtablezInfo* info, size_t hash, |
120 | size_t distance_from_desired); |
121 | |
122 | inline void RecordEraseSlow(HashtablezInfo* info) { |
123 | info->size.fetch_sub(1, std::memory_order_relaxed); |
124 | info->num_erases.fetch_add(1, std::memory_order_relaxed); |
125 | } |
126 | |
127 | HashtablezInfo* SampleSlow(int64_t* next_sample); |
128 | void UnsampleSlow(HashtablezInfo* info); |
129 | |
130 | class HashtablezInfoHandle { |
131 | public: |
132 | explicit HashtablezInfoHandle() : info_(nullptr) {} |
133 | explicit HashtablezInfoHandle(HashtablezInfo* info) : info_(info) {} |
134 | ~HashtablezInfoHandle() { |
135 | if (ABSL_PREDICT_TRUE(info_ == nullptr)) return; |
136 | UnsampleSlow(info_); |
137 | } |
138 | |
139 | HashtablezInfoHandle(const HashtablezInfoHandle&) = delete; |
140 | HashtablezInfoHandle& operator=(const HashtablezInfoHandle&) = delete; |
141 | |
142 | HashtablezInfoHandle(HashtablezInfoHandle&& o) noexcept |
143 | : info_(absl::exchange(o.info_, nullptr)) {} |
144 | HashtablezInfoHandle& operator=(HashtablezInfoHandle&& o) noexcept { |
145 | if (ABSL_PREDICT_FALSE(info_ != nullptr)) { |
146 | UnsampleSlow(info_); |
147 | } |
148 | info_ = absl::exchange(o.info_, nullptr); |
149 | return *this; |
150 | } |
151 | |
152 | inline void RecordStorageChanged(size_t size, size_t capacity) { |
153 | if (ABSL_PREDICT_TRUE(info_ == nullptr)) return; |
154 | RecordStorageChangedSlow(info_, size, capacity); |
155 | } |
156 | |
157 | inline void RecordRehash(size_t total_probe_length) { |
158 | if (ABSL_PREDICT_TRUE(info_ == nullptr)) return; |
159 | RecordRehashSlow(info_, total_probe_length); |
160 | } |
161 | |
162 | inline void RecordInsert(size_t hash, size_t distance_from_desired) { |
163 | if (ABSL_PREDICT_TRUE(info_ == nullptr)) return; |
164 | RecordInsertSlow(info_, hash, distance_from_desired); |
165 | } |
166 | |
167 | inline void RecordErase() { |
168 | if (ABSL_PREDICT_TRUE(info_ == nullptr)) return; |
169 | RecordEraseSlow(info_); |
170 | } |
171 | |
172 | friend inline void swap(HashtablezInfoHandle& lhs, |
173 | HashtablezInfoHandle& rhs) { |
174 | std::swap(lhs.info_, rhs.info_); |
175 | } |
176 | |
177 | private: |
178 | friend class HashtablezInfoHandlePeer; |
179 | HashtablezInfo* info_; |
180 | }; |
181 | |
182 | #if ABSL_PER_THREAD_TLS == 1 |
183 | extern ABSL_PER_THREAD_TLS_KEYWORD int64_t global_next_sample; |
184 | #endif // ABSL_PER_THREAD_TLS |
185 | |
186 | // Returns an RAII sampling handle that manages registration and unregistation |
187 | // with the global sampler. |
188 | inline HashtablezInfoHandle Sample() { |
189 | #if ABSL_PER_THREAD_TLS == 0 |
190 | static auto* mu = new absl::Mutex; |
191 | static int64_t global_next_sample = 0; |
192 | absl::MutexLock l(mu); |
193 | #endif // !ABSL_HAVE_THREAD_LOCAL |
194 | |
195 | if (ABSL_PREDICT_TRUE(--global_next_sample > 0)) { |
196 | return HashtablezInfoHandle(nullptr); |
197 | } |
198 | return HashtablezInfoHandle(SampleSlow(&global_next_sample)); |
199 | } |
200 | |
201 | // Holds samples and their associated stack traces with a soft limit of |
202 | // `SetHashtablezMaxSamples()`. |
203 | // |
204 | // Thread safe. |
205 | class HashtablezSampler { |
206 | public: |
207 | // Returns a global Sampler. |
208 | static HashtablezSampler& Global(); |
209 | |
210 | HashtablezSampler(); |
211 | ~HashtablezSampler(); |
212 | |
213 | // Registers for sampling. Returns an opaque registration info. |
214 | HashtablezInfo* Register(); |
215 | |
216 | // Unregisters the sample. |
217 | void Unregister(HashtablezInfo* sample); |
218 | |
219 | // The dispose callback will be called on all samples the moment they are |
220 | // being unregistered. Only affects samples that are unregistered after the |
221 | // callback has been set. |
222 | // Returns the previous callback. |
223 | using DisposeCallback = void (*)(const HashtablezInfo&); |
224 | DisposeCallback SetDisposeCallback(DisposeCallback f); |
225 | |
226 | // Iterates over all the registered `StackInfo`s. Returning the number of |
227 | // samples that have been dropped. |
228 | int64_t Iterate(const std::function<void(const HashtablezInfo& stack)>& f); |
229 | |
230 | private: |
231 | void PushNew(HashtablezInfo* sample); |
232 | void PushDead(HashtablezInfo* sample); |
233 | HashtablezInfo* PopDead(); |
234 | |
235 | std::atomic<size_t> dropped_samples_; |
236 | std::atomic<size_t> size_estimate_; |
237 | |
238 | // Intrusive lock free linked lists for tracking samples. |
239 | // |
240 | // `all_` records all samples (they are never removed from this list) and is |
241 | // terminated with a `nullptr`. |
242 | // |
243 | // `graveyard_.dead` is a circular linked list. When it is empty, |
244 | // `graveyard_.dead == &graveyard`. The list is circular so that |
245 | // every item on it (even the last) has a non-null dead pointer. This allows |
246 | // `Iterate` to determine if a given sample is live or dead using only |
247 | // information on the sample itself. |
248 | // |
249 | // For example, nodes [A, B, C, D, E] with [A, C, E] alive and [B, D] dead |
250 | // looks like this (G is the Graveyard): |
251 | // |
252 | // +---+ +---+ +---+ +---+ +---+ |
253 | // all -->| A |--->| B |--->| C |--->| D |--->| E | |
254 | // | | | | | | | | | | |
255 | // +---+ | | +->| |-+ | | +->| |-+ | | |
256 | // | G | +---+ | +---+ | +---+ | +---+ | +---+ |
257 | // | | | | | | |
258 | // | | --------+ +--------+ | |
259 | // +---+ | |
260 | // ^ | |
261 | // +--------------------------------------+ |
262 | // |
263 | std::atomic<HashtablezInfo*> all_; |
264 | HashtablezInfo graveyard_; |
265 | |
266 | std::atomic<DisposeCallback> dispose_; |
267 | }; |
268 | |
269 | // Enables or disables sampling for Swiss tables. |
270 | void SetHashtablezEnabled(bool enabled); |
271 | |
272 | // Sets the rate at which Swiss tables will be sampled. |
273 | void SetHashtablezSampleParameter(int32_t rate); |
274 | |
275 | // Sets a soft max for the number of samples that will be kept. |
276 | void SetHashtablezMaxSamples(int32_t max); |
277 | |
278 | // Configuration override. |
279 | // This allows process-wide sampling without depending on order of |
280 | // initialization of static storage duration objects. |
281 | // The definition of this constant is weak, which allows us to inject a |
282 | // different value for it at link time. |
283 | extern "C" const bool kAbslContainerInternalSampleEverything; |
284 | |
285 | } // namespace container_internal |
286 | } // namespace absl |
287 | |
288 | #endif // ABSL_CONTAINER_INTERNAL_HASHTABLEZ_SAMPLER_H_ |
289 | |