Flow 2.0.0
Flow project: Full implementation reference.
histo.hpp
Go to the documentation of this file.
1/* Flow
2 * Copyright 2023 Akamai Technologies, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the
5 * "License"); you may not use this file except in
6 * compliance with the License. You may obtain a copy
7 * of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in
12 * writing, software distributed under the License is
13 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
14 * CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing
16 * permissions and limitations under the License. */
17
18/// @file
19#pragma once
20
23#include "flow/common.hpp"
24#include <vector>
25#include <type_traits>
26#include <limits>
27#include <atomic>
28
29namespace flow::util::stat
30{
31
32// Types.
33
34/**
35 * Stats counter: tracks a histogram that summarizes, cumulatively, the numeric outcome of an event that keeps
36 * repeatedly happening. The performance profile of this tracking is as follows: record_value() performs a small
37 * number of comparisons and additions, plus an increment. All memory allocation occurs in the ctor.
38 *
39 * ### Thread safety ###
40 * It is thread-safe, in that all operations on a given `*this` may be invoked concurrently, excluding
41 * those that could change the bucket structure (as of this writing copy-assignment).
42 *
43 * All reads and writes are done using `std::atomic` with `memory_order_relaxed`, meaning one can (at least
44 * given Linux x86-64) expect similar performance to regular loads/stores sans `atomic<>`. (The `++` op, for
45 * x86-64 -- `x.fetch_add(1)` -- does take a few more cycles on an `atomic` than a naked `++` would; it it not
46 * the identical processor instruction. However the cycle cost difference is in the low-single-digit-ns range
47 * when uncontended: not identical but close. That pertains to record_value(). Cf.: loads (count_for_bucket())
48 * are fully identical.)
49 *
50 * Due to the nature of stat-keeping, `memory_order_relaxed` semantics are usually considered semantically acceptable.
51 *
52 * ### How it works ###
53 * record_value() records an event, updating the histogram data which are cumulative
54 * (there is no sliding time window; it just keeps accumulating stats). The histogram results-so-far can be viewed
55 * at will via `const` access.
56 *
57 * Each event is assumed to result in an integer-number outcome which one passes to record_value(). For example
58 * it might be a die throw; in which cases the possible outcomes are 1, 2, ..., 6. We accept *any* outcome
59 * as long as it fits into the large-range, signed type #value_t; whether there is really a constraint like
60 * 1-to-6, as with a die throw, is not our worry per se. Well, sort of; read on.
61 *
62 * In the ctor, a histogram-bucket array is initialized. This determines, forever (until destruction or copy-to),
63 * the outcome buckets. A count (starting at 0 and then potentially incremented by record_value()) is
64 * stored for each bucket. Of the buckets each represents a grouping of possible outcomes. (We discuss potential
65 * bucket sizes and such and how to configure this separately below; but here we stay general.)
66 *
67 * There shall be N (at least 2) buckets. All buckets are adjacent (there are no #value_t holes in-between).
68 * Each bucket has width that is at least 1 (no null buckets). Therefore each bucket can be represented as
69 * a combination of the following:
70 * - Min-value: To land in the bucket, the outcome must be *at least* min-value.
71 * - Width. This determines max-value: Min-vale plus width minus 1. To land in the bucket, the outcome must be
72 * *at most* max-value. The next bucket, if any, begins at exactly max-value plus 1.
73 *
74 * This just leaves the aforementioned allowance of any input. Dice are not a great example for this, but just
75 * bear with us. Suppose you record_value() a number under first-bucket's min-value: in this case that's 1.
76 * Which bucket takes it? Answer: such an "underflow" always goes into first-bucket. So a "die throw" of 0
77 * or -12312 goes into first-bucket. Similarly, any value greater than or equal to X, where X is the min-value for
78 * last-bucket, lands into last-bucket (even if it exceeds last-bucket's max-value). A "die throw" of 7 or 321
79 * land in last-bucket, along with the vanilla last-bucket throw 6.
80 *
81 * @note You might notice that this in effect makes first-bucket's min-value and last-bucket's max-value
82 * irrelevant; as long as, when invoking ctor, it possible to express everything about the buckets except
83 * those two values, then it'll all work fine. Indeed: this is true. Nevertheless we require these
84 * things (one way or another) to still be specified. Reasons: First-bucket min-value provides a user-friendly
85 * "anchor point" of sorts, from which one can express various required points (e.g., first-bucket max-value,
86 * which is mandatory, can be expressed as min-value plus bucket-width minus 1). Last-bucket max-value
87 * is less useful in that sense but provides user-friendly symmetry (and the ability to detect overflow events
88 * in the future, if we come to desire this feature). Lastly, these values are -- again for user-friendliness
89 * reasons -- shown in the pretty-print output (`ostream<<` + to_ostream()).
90 *
91 * What about specific bucket structures supported by `*this`? In fact a `*this` supports any conceivable bucket
92 * structure, as long as the basic axioms are observed (to recap: at least 2, no null buckets, buckets are adjacent).
93 * In other words, the N buckets can be of any widths; e.g., they can be all different. The `vector`-taking
94 * ctor form supports this maximally-flexible config. However, there are different ctor forms for convenience
95 * and performance. We discuss these now.
96 *
97 * ### Bucket structure: Linear ###
98 * The simplest bucket structure supported is *linear*. In this setup, first-bucket can be of any specified
99 * width (as usual, not zero); while the remaining buckets (1+ of them) shall each have the same specified
100 * width. Therefore, the scale is generally linear, with a special-case potentially-different first-bucket.
101 *
102 * The rationale for offering this ctor form is not just convenience in the case where one happens to desire a
103 * linear scale. It is also, internally, handled differently w/r/t *performance*. If the bucket structure is
104 * linear, then the only computationally notable operation within record_value() et al -- given `value_t` outcome,
105 * find `size_t` bucket index of bucket containing that out come -- can be performed in constant time; no
106 * search through the buckets is required. record_value() can record stats inside tight loops in the middle
107 * of very perf-sensitive areas of one's application; the faster it is the better. This is therefore a significant
108 * win (when applicable).
109 *
110 * @note This constant-time-performance gain is in effect whenever the configured bucket structure supports it;
111 * it is not necessary to use that particular linear-scale ctor form. That is, if one uses the general
112 * (`vector`-taking) ctor form, and using that form specifies a linear-scale bucket structure, then
113 * `*this` will automatically use the faster lookup algorithm in record_value() et al.
114 *
115 * @note For the constant-time-performance gain to activate the requirement is as follows:
116 * Either there are exactly 2 buckets (degenerate case); or there are 3+, and all non-bookend buckets
117 * (i.e., all but first-bucket and last-bucket) have equal widths. (The linear-scale ctor form
118 * guarantees this; though in its case last-bucket also has the same width as the non-bookend buckets.)
119 *
120 * We demonstrate the linear-scale ctor's knobs with the die-throw example. `n_buckets` (ctor arg)
121 * is how many buckets there are. `bucket0_sz` is the "width" of first-bucket, and `bucket_sz` is the "width"
122 * of each of the other buckets including last-bucket. `bucket0_val0` is the min-value for first-bucket.
123 * - For example, with a die throw, you might use `n_buckets = 6` and `bucket0_sz = bucket_sz = bucket0_val0 = 1`.
124 * - The outcomes are then simply grouped as: [got 1][got 2][got 3][got 4][got 5][got 6].
125 * - Or you could do: `n_buckets = 3` and `bucket0_sz = bucket_sz = 2` and `bucket0_val0 = 1`.
126 * - The buckets are then: [got 1 or 2][got 3 or 4][got 5 or 6].
127 *
128 * This just leaves the aforementioned allowance of any input. Dice are not a great example for this, but just
129 * bear with us. Suppose you record_value() a number under `bucket0_val0`. Which bucket takes it? Answer:
130 * such an "underflow" always goes into first-bucket. So a "die throw" of 0 or -12312 goes into first-bucket.
131 * Similarly, any value greater than or equal to X, where X is the min-value for last-bucket, lands into last-bucket.
132 * - So consider `n_buckets = 3` and `bucket0_sz = bucket_sz = 2` and `bucket0_val0 = 1`. This *really* means
133 * the following actual buckets as viewed by `*this`:
134 * - [any value <= 2][3 or 4][any value >= 5].
135 * - In reality valid dice throws always result in 1 or 2 or ... or 6, so this happens to reduce to
136 * [1 or 2][3 or 4][5 or 6]. If "broken" dice throws are possible, though, then a `*this` stands ready
137 * to count them in the two edge buckets.
138 *
139 * ### Bucket structure: General ###
140 * Other than that, the general structure allows one to simply specify each bucket's width and min-value.
141 * The general-scale ctor form takes a `vector` (sized at least 2, the # of buckets); each bucket's min-value
142 * is that element (so they must be in strictly-increasing order). The only missing bit of info is the
143 * last-bucket's width (as there's no next-bucket to point at where this one ends); this is supplied as a separate
144 * ctor arg alongside the `vector`.
145 *
146 * Example: With die-throws, the general-scale ctor call would look like so:
147 * `Histogram_counter{{1, 2, 3, 4, 5, 6}, 1}`. Six buckets, with the given min-values; and the last bucket's
148 * width is 1.
149 *
150 * The performance of bucket-index-from-outcome lookup in record_value() et al is therefore as follows:
151 * - If 2 buckets: constant-time (degenerate case).
152 * - If 3+ buckets, and the non-bookend buckets are all of the same width: constant-time (see above).
153 * - Otherwise: logarithmic in bucket-count. The lookup is a straightforward binary search through
154 * the min-values `vector` supplied via ctor.
155 *
156 * @internal
157 * ### Impl notes ###
158 * Reading the ground-level comments should clarify ~all questions.
159 *
160 * One mechanical subtlety is that we use stat::load(), stat::store(), and stat::fetch_add() as our atomic ops.
161 * These are respectively simply:
162 * - load(x) is `x.load(memory_order_relaxed)`.
163 * - store(&x, y) is `x.store(y, memory_order_relaxed).
164 * - fetch_add(&x, y) is `x.fetch_add(y, memory_order_relaxed)`.
165 *
166 * We promised `memory_order_relaxed` perf and semantics; and those ops are available in flow::util::stat.
167 */
169{
170public:
171 // Types.
172
173 /// Each event outcome recorded by record_value() must fit in the range of this type.
174 using value_t = int64_t;
175 static_assert(std::is_integral_v<value_t> && std::is_signed_v<value_t>,
176 "Histogram_counter shall support negative outcomes albeit only integer ones.");
177
178 /// The count type.
179 using count_t = uint64_t;
180
181 // Constructors/destructor.
182
183 /**
184 * Allocates the histogram data structure (linear-scale: bucket widths [ABBB...]) and initializes each bucket's count
185 * to zero. Bucket widths [ABBB...] means that first-bucket can be of any positive length, while the remaining
186 * 1+ buckets are each of the same width which may or not equal first-bucket's.
187 *
188 * ### Performance ###
189 * This ctor form guarantees constant-time record_value(). See class doc header for a deeper dive.
190 *
191 * @see class doc header for overview of the arg knobs and the overall algorithm they control. Meanwhile
192 * the docs just below are formal with few-or-no expositional niceties.
193 *
194 * @param n_buckets
195 * Number of buckets (at least 2) in the histogram.
196 * @param bucket0_sz
197 * First-bucket shall count the following outcomes:
198 * less-than `bucket0_val0`, `bucket0_val0`, ..., `bucket0_val0 + bucket0_sz - 1`.
199 * `bucket0_sz` is at least 1.
200 * @param bucket_sz
201 * Each bucket except first-bucket and last-bucket shall count outcomes `X`, ..., `X + bucket_sz - 1`,
202 * where `X` is one plus the max-value of the preceding bucket.
203 * Last-bucket shall count outcomes `X`, `X + 1`, ... ad infinitum, where `X` is one plus the max-value of
204 * the preceding (next-to-last) bucket.
205 * `bucket_sz` is at least 1.
206 * @param bucket0_val0
207 * See above. Note that any value is allowed for `bucket0_val0`.
208 */
209 explicit Histogram_counter(size_t n_buckets, value_t bucket0_sz, value_t bucket_sz = 1, value_t bucket0_val0 = 0);
210
211 /**
212 * Allocates the histogram data structure (general-scale: bucket widths [ABCD...]) and initializes each bucket's
213 * count to zero. Bucket widths [ABCD...] means that each bucket's width can be any positive value, and
214 * they may (but are not required to) be all different.
215 *
216 * @note `bucket_val0s` can often be specified as a naked initializer-list sans any mention of `std::vector`;
217 * e.g.: `Histogram_counter histo{{1, 3, 5}, 2};`.
218 *
219 * ### Performance ###
220 * If all bucket widths, except possibly the first and last, are all equal -- or there are only 2 buckets total --
221 * this ctor form guarantees constant-time record_value(). Otherwise record_value() shall be logarithmic-time in
222 * the number of buckets. See class doc header for a deeper dive.
223 *
224 * @see class doc header for overview of the arg knobs and the overall algorithm they control. Meanwhile
225 * the docs just below are formal with few-or-no expositional niceties.
226 *
227 * @param bucket_val0s
228 * Vector (of size at least 2) indicating the min-values of each bucket in order; these must be in
229 * strictly increasing order. The max-value of the last bucket shall be
230 * `bucket_val0s.back() + bucket_n_sz - 1`.
231 * First-bucket shall *also* count all outcomes less-than its min-value (but to_ostream()
232 * will still specially show this min-value for expositional purposes).
233 * Last-bucket shall *also* count all outcomes greater-than the indicated max-value (but to_ostream()
234 * will still specially show this max-value for expositional purposes).
235 * @param bucket_n_sz
236 * Width (at least 1) of the last bucket; see formula just above.
237 */
238 explicit Histogram_counter(const std::vector<value_t>& bucket_val0s, value_t bucket_n_sz);
239
240 /**
241 * Straightforward copy ctor. There is no special-behavior move ctor.
242 * @param src
243 * Source object.
244 */
246
247 // Methods.
248
249 /**
250 * Straightforward copy assignment. There is no special-behavior move assignment.
251 * @param src
252 * Source object.
253 * @return `*this`.
254 */
256
257 /**
258 * Aggregator: bucket-by-bucket, makes `*this` bucket's count incremented by `src`'s corresponding bucket's
259 * count. That is: make `this->count_for_bucket(idx) == C(idx) + S(idx)`, where `C(idx)` is
260 * pre-condition `this->count_for_bucket(idx)`, and `S` is ditto for `src`.
261 *
262 * If `src` has a different structure than `*this`, behavior is undefined. Different structure means
263 * the following does not hold: same bucket count; for each respective bucket min-value and width are the same.
264 *
265 * Tip: To aggregate `N`=2+ `Histogram_counters`:
266 * - (Destructive) Take `auto& result = v[0]`; then for `idx` in [`1`, `N - 1`]: `result += v[idx]`.
267 * - (Non-destructive) Take `auto result = v[0]`; then for `idx` in [`1`, `N - 1`]: `result += v[idx]`.
268 *
269 * @param src
270 * One with same structure as `*this`. `this == &src` is allowed and will incur no special handling
271 * (hence the counts shall be doubled).
272 * @return `*this`.
273 */
275
276 /**
277 * Delta: bucket-by-bucket, makes `*this` bucket's count decremented by `src`'s corresponding bucket's
278 * count. That is: make `this->count_for_bucket(idx) == C(idx) - S(idx)`, where `C(idx)` is
279 * pre-condition `this->count_for_bucket(idx)`, and `S` is ditto for `src`.
280 *
281 * @warning There is no underflow protection; if a given `C(idx) < S(idx)`, then post-condition
282 * `C(idx)` shall potentially *increase* in apparent value, potentially to an extremely high value
283 * indeed.
284 *
285 * If `src` has a different structure than `*this`, behavior is undefined. Different structure means
286 * the following does not hold: same bucket count; for each respective bucket min-value and width are the same.
287 *
288 * @param src
289 * One with same structure as `*this`. (`this == &src` is allowed and will incur no special handling
290 * (hence the counts shall be zeroed). Though in that case it's better to use clear().)
291 * @return `*this`.
292 */
294
295 /**
296 * Scaler: bucket-by-bucket, makes `this->count_for_bucket(idx)` equal to `v / divisor`, where `v` is its
297 * pre-condition value.
298 *
299 * Tip: Can compute a mean histogram as follows: operator+=() `N - 1` times, then `*this /= N`.
300 *
301 * @param divisor
302 * Non-zero divisor. If zero undefined behavior results (assertion may trip).
303 * @return `*this`.
304 */
306
307 /**
308 * Record event outcome. See class doc header and ctor doc header for details.
309 *
310 * @tparam T
311 * `val` shall be `static_cast` from `T` to #value_t.
312 * @param val
313 * The outcome.
314 */
315 template<typename T>
316 void record_value(T val);
317
318 /**
319 * Convenience wrapper for record_value() that records a duration in the units specified as the mandatory
320 * first template parameter.
321 *
322 * If necessary to lose precision, the duration is rounded to the nearest
323 * desired unit.
324 *
325 * @todo It is possible to make util::stat::Histogram_counter a template parameterized on the type of values
326 * stored, which would in particular allow `chrono::duration`s or `Fine_duration`s to be stored instead
327 * of signed large integers.
328 *
329 * @internal
330 * @note Internal impl note: For the preceding to-do impl might involve some specializing or otherwise wrangling the
331 * arithmetic in key private helper idx_of_outcome().
332 * @endinternal
333 *
334 * @note If different rounding behavior is desired, such as floor or ceiling, then please use record_value()
335 * directly along with the appropriate rounding function: `.record_value(ceil<seconds>(val).count())`.
336 *
337 * @tparam Recorded_duration
338 * `boost::chrono` concrete duration type specifying the units in which the histogram tracks durations.
339 * E.g.: `chrono::milliseconds`, `chrono::minutes`.
340 * @tparam Duration
341 * Any `boost::chrono` duration type, typically auto-detected by the compiler.
342 * @param val
343 * The outcome duration.
344 */
345 template<typename Recorded_duration, typename Duration>
346 void record_period(Duration val);
347
348 /**
349 * Returns the number of buckets in the histogram, as supplied to ctor as `n_buckets`. Naming is consistent with
350 * standard container `size()` (the bucket structure being the structure of `*this`); not to be confused with
351 * the count-per-bucket via count_for_bucket().
352 *
353 * @return Number of buckets in `*this`.
354 */
355 size_t size() const;
356
357 /**
358 * Returns the event count accumulated so far in the bucket with the given index.
359 * The semantics of how buckets are ordered and what they track are explained in the class doc header; or more
360 * formally in the ctor doc header.
361 *
362 * @param idx
363 * Bucket index. Behavior undefined (assert may trip) unless this is
364 * in [`0`, size()), where size() equals `n_buckets` supplied in ctor.
365 * @return The count so far. Note it cannot overflow due to the gargantuan max-value of #count_t.
366 */
367 count_t count_for_bucket(size_t idx) const;
368
369 /**
370 * Returns `count_for_bucket(idx)`, where `idx` is the index of the bucket into which `val` would fall, if one were
371 * to call `record_value(val)`.
372 *
373 * This method may be convenient, but please remember: This doesn't count how many times `val` outcome has
374 * occurred alone; it also includes the range of outcomes in its bucket; depending on the knobs set in ctor
375 * other outcomes may have been possible.
376 *
377 * @tparam T
378 * See record_value().
379 * @param val
380 * See record_value().
381 * @return See count_for_bucket().
382 */
383 template<typename T>
385
386 /// Returns `*this` to the state as-if just-cted with the current bucket structure. Uses stat::store() x N.
387 void clear();
388
389 /**
390 * Set a bucket-value so that `count_for_bucket(idx) == new_count`.
391 *
392 * ### Rationale ###
393 * If counting the usual `record_*()` techniques, the use of overwrite_count_for_bucket() is arguably unlikely.
394 * The precipitating use case is when the counts come wholesale from another stats data-source, so that
395 * a Histogram_counter is used as a mere container of counts, perhaps for pretty-printing and/or
396 * with stats_since_reset_state().
397 *
398 * Corollary: Be cautious if concurrent access -- particularly record_value() et al -- is possible.
399 * Uses stat::store().
400 *
401 * @param idx
402 * See record_value().
403 * @param new_count
404 * See count_for_bucket(): namely what it returns.
405 */
406 void overwrite_count_for_bucket(size_t idx, count_t new_count);
407
408 /**
409 * Prints string representation to the given `ostream`.
410 *
411 * @param os
412 * Stream to which to write.
413 */
414 void to_ostream(std::ostream* os) const;
415
416private:
417 // Methods.
418
419 /**
420 * Which index into #m_per_bucket_counts points to the appropriate bucket for `record_value(val)`?
421 *
422 * @param val
423 * See record_value().
424 * @return Index into #m_per_bucket_counts.
425 */
426 size_t idx_of_outcome(value_t val) const;
427
428 // Data.
429
430 /**
431 * General bucket structure (except the last bucket's width): in strictly increasing order, each bucket's
432 * min-value. Not `const` to allow for copy-assignment at least.
433 *
434 * ### Use 1: Output ###
435 * There are two uses. First and least, it plus #m_bucket_n_sz are used in to_ostream() for user-friendly output,
436 * labeling the range of each bucket.
437 *
438 * ### Use 2: idx_of_outcome() ###
439 * More importantly it is used to implement idx_of_outcome(), used by record_value() et al to determine which
440 * bucket's count to increment, given an outcome belonging to one of said buckets. That is the central
441 * and only potentially time-consuming of the fast-path ops in a `*this`. The following cases are to be
442 * considered in order for a good combo of performance (top priority) and simplicity.
443 * -# **If `size() == 2`**
444 * Only `m_bucket_val0s[1]` matters: Outcomes less-than this min-value are in bucket 0;
445 * else in bucket 1. Various corner cases can be ignored if we handle this particular degenerate case
446 * separately.
447 * - Time complexity: constant.
448 * -# **If #m_linear_bucket_sz_or_0 is in effect (`!= 0`)**
449 * This means all 1+ buckets strictly-between 1st and last are of the same width (vacuously so if `size() == 3`);
450 * namely `m_linear_bucket_sz_or_0`. Therefore an optimized algorithm for idx_of_outcome() is in effect; no search
451 * through buckets is required. Instead: if below `F = m_bucket_val0s[1]` => index 0; otherwise subtract `F`,
452 * divide by `m_linear_bucket_sz_or_0`, and add 1. The result, clipped against the ceiling `size() - 1`, is
453 * the index sought.
454 * - Time complexity: constant.
455 * -# **General case**
456 * No particular optimization applies. Perform a binary-search through #m_bucket_val0s, clip against
457 * the range [`0`, `size()`), and voila.
458 * - Time complexity: logarithmic in size().
459 *
460 * ### Impl rationale: Why no `flat_map` instead of 2 `vector`s? ###
461 * We keep two equally-sized `vector`s: #m_bucket_val0s (the "labels": the bucket structure) and #m_per_bucket_counts
462 * (the counts in those respective buckets). `flat_map<value_t, atomic<count_t>>` would achieve essentially the
463 * same thing; in terms of time complexity of all our ops definitely the case. So should we use that instead?
464 *
465 * Firstly: yes, it would be quite defensible to do that. In terms of internal code quality, it would be more
466 * compact; things like `flat_map::upper_bound()` => iterator => deref directly in map => increment `.second`
467 * would read as tighter than the alternative -- a bit nicer. And the perf difference below, while real, is
468 * still pretty small.
469 *
470 * So why go with 2 `vector`s? Answer: basically it's a perf-maxing thing. record_value() et al can be and are
471 * used in extremely perf-sensitive tight loops, so potentially every processor cycle counts. The 2-`vector`
472 * setup has optimal cache-locality (#m_per_bucket_counts is what changes and is all in one area; #m_bucket_val0s
473 * is elsewhere and does not change after construction or copy-to). So by doing this we do guarantee the best
474 * perf achievable (in context).
475 *
476 * That said: Who says `flat_map` does not do it the same anyway? And, actually, it *sort of* does. In fact:
477 * `std::flat_map` is specified by the standard as an adapter that defaults to two internal containers
478 * `vector<Key>`, `vector<Mapped>` -- *exactly* what we have. However, as of this writing we are constrained
479 * by C++17; so we cannot use `std::flat_map`. What about `boost::container::flat_map` which originated `flat_map`
480 * in the first place, though? Answer: Interestingly, as of mid-2026, `boost::container::flat_map` is
481 * actually somewhat different from what the standard mandates for `std::flat_map`. It is not an adapter --
482 * its internal representation is hard-coded -- and looking inside there shows it deals in `pair<Key, Mapped>`;
483 * so most likely not keeping the keys and mapped-values segregated.
484 */
485 std::vector<value_t> m_bucket_val0s;
486
487 /**
488 * Bucket structure: an addendum to #m_bucket_val0s, this is the positive width of the last of size() buckets.
489 *
490 * ### Use ###
491 * It is used with #m_bucket_val0s in to_ostream() for user-friendly output.
492 *
493 * Note that it is *not* used in idx_of_outcome(), as any outcome not-below min-value of the last bucket
494 * is dumped into the last bucket. Again, though, it is nice for printing `*this`.
495 */
497
498 /**
499 * If `size() == 2`, or the non-bookend buckets are not all of the same width, this is zero; else
500 * the (positive) width of each non-bookend bucket. In the latter case idx_of_outcome() may use the
501 * optimized non-degenerate constant-time algorithm that sidesteps the need to binary-search #m_bucket_val0s.
502 *
503 * @see #m_bucket_val0s doc header for an outline of the idx_of_outcome() algorithm, including how it
504 * aims to prominently use #m_linear_bucket_sz_or_0.
505 *
506 * ### Rationale ###
507 * Why keep this, given that (assuming it is not zero) it can be quickly computed
508 * as `(m_bucket_val0s[2] - m_bucket_val0s[1])`? Answer: For one thing, it acts as a flag to cache whether
509 * the non-bookend buckets are indeed all equal (walking #m_bucket_val0s each time would be linear-time and
510 * destroy the whole point of the intended optimization). Opportunistically, then, we might as well cache
511 * the aforementioned value to avoid the need for recomputing it; we've got to store the flag in any case so
512 * might as well cut down on repeated computations.
513 */
515
516 /**
517 * The data. Elements start at 0 and are incremented indefinitely; #count_t is too wide for overflow to be ever
518 * reached. `.size() >= 2`.
519 *
520 * Storing a `vector<atomic<>>` can be dodgy, in that it can be tough to even get it to compile, but in our case:
521 * - (Ignoring copy-assignment) It is constructed at its forever-`.capacity()` (and `.size()`) all the way
522 * until it is destroyed. No problems there.
523 * - Default copy-assignment would not compile; therefore our copy-assignment operator performs the required
524 * re-construction at the new capacity/size (explicitly); and then performs the required atomic-load-and-store
525 * ops (explicitly).
526 * - Copy-construction reuses this (again: default copy-assignment would not compile).
527 */
528 std::vector<std::atomic<count_t>> m_per_bucket_counts;
529}; // class Histogram_counter
530
531// Free functions: in *_fwd.hpp.
532
533// Template implementations.
534
535template<typename T>
537{
538 auto& count = m_per_bucket_counts[idx_of_outcome(static_cast<value_t>(val))];
539
540#if 1
541 fetch_add(&count, 1);
542
543 static_assert(sizeof(count_t) >= 8,
544 "We must use 64+-bit counters, so as to not need to contend with overflow.");
545#else // Pre-atomic<> code is here for posterity. With atomic<> the overflow case is just a pain, so we do the above.
546 if constexpr(sizeof(count_t) <= 4)
547 {
548 if (count != std::numeric_limits<count_t>::max()) // We promised to stop just before overflow.
549 {
550 ++count;
551 }
552 }
553 else
554 {
555 ++count; // In practice with 64+ bits in a count, it'll never overflow, so don't even worry about that.
556 }
557#endif
558}
559
560template<typename Recorded_duration, typename Duration>
562{
563 record_value(boost::chrono::round<Recorded_duration>(val).count());
564}
565
566template<typename T>
568{
569 return load(m_per_bucket_counts[idx_of_outcome(static_cast<value_t>(val))]);
570}
571
572} // namespace flow::util::stat
Stats counter: tracks a histogram that summarizes, cumulatively, the numeric outcome of an event that...
Definition: histo.hpp:169
count_t count_for_bucket(size_t idx) const
Returns the event count accumulated so far in the bucket with the given index.
Definition: histo.cpp:279
void record_period(Duration val)
Convenience wrapper for record_value() that records a duration in the units specified as the mandator...
Definition: histo.hpp:561
size_t idx_of_outcome(value_t val) const
Which index into m_per_bucket_counts points to the appropriate bucket for record_value(val)?
Definition: histo.cpp:178
Histogram_counter & operator-=(const Histogram_counter &src)
Delta: bucket-by-bucket, makes *this bucket's count decremented by src's corresponding bucket's count...
Definition: histo.cpp:150
std::vector< std::atomic< count_t > > m_per_bucket_counts
The data.
Definition: histo.hpp:528
Histogram_counter & operator=(const Histogram_counter &src)
Straightforward copy assignment.
Definition: histo.cpp:109
Histogram_counter & operator/=(count_t divisor)
Scaler: bucket-by-bucket, makes this->count_for_bucket(idx) equal to v / divisor, where v is its pre-...
Definition: histo.cpp:165
void record_value(T val)
Record event outcome.
Definition: histo.hpp:536
value_t m_bucket_n_sz
Bucket structure: an addendum to m_bucket_val0s, this is the positive width of the last of size() buc...
Definition: histo.hpp:496
value_t m_linear_bucket_sz_or_0
If size() == 2, or the non-bookend buckets are not all of the same width, this is zero; else the (pos...
Definition: histo.hpp:514
uint64_t count_t
The count type.
Definition: histo.hpp:179
size_t size() const
Returns the number of buckets in the histogram, as supplied to ctor as n_buckets.
Definition: histo.cpp:274
int64_t value_t
Each event outcome recorded by record_value() must fit in the range of this type.
Definition: histo.hpp:174
void clear()
Returns *this to the state as-if just-cted with the current bucket structure. Uses stat::store() x N.
Definition: histo.cpp:285
Histogram_counter & operator+=(const Histogram_counter &src)
Aggregator: bucket-by-bucket, makes *this bucket's count incremented by src's corresponding bucket's ...
Definition: histo.cpp:135
void to_ostream(std::ostream *os) const
Prints string representation to the given ostream.
Definition: histo.cpp:299
Histogram_counter(size_t n_buckets, value_t bucket0_sz, value_t bucket_sz=1, value_t bucket0_val0=0)
Allocates the histogram data structure (linear-scale: bucket widths [ABBB...]) and initializes each b...
Definition: histo.cpp:74
void overwrite_count_for_bucket(size_t idx, count_t new_count)
Set a bucket-value so that count_for_bucket(idx) == new_count.
Definition: histo.cpp:293
std::vector< value_t > m_bucket_val0s
General bucket structure (except the last bucket's width): in strictly increasing order,...
Definition: histo.hpp:485
count_t count_for_bucket_containing_outcome(T val) const
Returns count_for_bucket(idx), where idx is the index of the bucket into which val would fall,...
Definition: histo.hpp:567
Fine_duration Duration
Short-hand for a high-precision boost.chrono duration, formally equivalent to flow::Fine_duration.
Sub-module of flow::util providing lightweight facilities for working with simple structs of stat cou...
Definition: histo.cpp:26
const T & load(const T &val)
Default load(stats.m_val) that simply return stats.m_val.
Definition: stat_set.hpp:332
void fetch_add(T *target_val, const boost::type_identity_t< T > &addend_val)
Default fetch_add(&target_stats.m_val, addend_val) that simply applies *target_val += addend_val.
Definition: stat_set.hpp:356