Flow 2.0.0
Flow project: Full implementation reference.
histo.cpp
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
22#include <cassert>
23#include <algorithm>
24
26{
27
28// Implementations.
29
30Histogram_counter::Histogram_counter(const std::vector<value_t>& bucket_val0s, value_t bucket_n_sz) :
31 m_bucket_val0s(bucket_val0s),
32 m_bucket_n_sz(bucket_n_sz),
33 m_linear_bucket_sz_or_0(0), // Might stay this way (general case; degenerate case size() == 2); or not (see below).
34 m_per_bucket_counts(bucket_val0s.size())
35{
36 using std::adjacent_find;
37 using std::greater_equal;
38
39 // See m_bucket_val0s doc header for the central algorithm's overview; it informs the below.
40
41 const auto start_it = m_bucket_val0s.begin();
42 const auto end_it = m_bucket_val0s.end();
43 assert((m_bucket_val0s.size() > 1) && "Ctor contract violation.");
44 assert((m_bucket_n_sz > 0) && "Ctor contract violation.");
45 assert((adjacent_find(start_it, end_it, greater_equal<value_t>()) == end_it)
46 && "Ctor contract violation: Needs to be sorted and sans repeats.");
47
48 const auto n_buckets = size();
49 if (n_buckets != 2) // I.e., n_buckets >= 3.
50 {
51 // Optimism! Assume constant-time idx_of_outcome(). Grab 1st middle-width W=[1.min->2.min]; assume rest are same.
53
54 /* Check whether assumption holds up: W=[2.min->3.min], W=[3.min->4.min], etc.
55 * - If n_buckets=3, adjacent_find() is given a 1-range, so it just returns .end(). Vacuously there is just W.
56 * - If n_buckets>4, adjacent_find() is given a >1-range, so it scans at least 1 adjacent pair; if any !=W,
57 * returns non-.end().
58 * - If it can't find such a thing -- meaning all are =W -- then returns .end(). */
59 if (end_it != adjacent_find(start_it + 2, end_it,
60 [&](auto a, auto b) -> bool { return (b - a) != m_linear_bucket_sz_or_0; }))
61 {
62 m_linear_bucket_sz_or_0 = 0; // Optimism failed! One of the middle bkt widths was not the same as all others.
63 }
64 // else { N=3; or N>3, and each middle-width after the 1st (W) equals W. Optimism justified! }
65 }
66 // else if (2 buckets) { Degenerate case of idx_of_outcome(). }
67
68 /* Before C++20 the atomic<>s are originally uninitialized... or at least there are mixed signals out there
69 * about whether a given impl will do the right thing here in the context of vector<atomic<>>.
70 * Let's not risk any issues. @todo Remove on moving to C++20. */
71 clear();
72} // Histogram_counter::Histogram_counter()
73
74Histogram_counter::Histogram_counter(size_t n_buckets, value_t bucket0_sz, value_t bucket_sz, value_t bucket0_val0) :
75 m_bucket_val0s(n_buckets), // size() set; must still set the values (min-value for each bucket).
76 m_bucket_n_sz(bucket_sz),
77 m_linear_bucket_sz_or_0(bucket_sz),
78 m_per_bucket_counts(n_buckets)
79{
80 // See m_bucket_val0s doc header for the central algorithm's overview; it informs the below.
81
82 assert((n_buckets > 1) && "Ctor contract violation.");
83 assert((bucket0_sz > 0) && "Ctor contract violation.");
84 assert((bucket_sz > 0) && "Ctor contract violation.");
85
86 /* Maintain official invariant per m_linear_bucket_sz_or_0 doc header. (It is mostly for cleanliness here;
87 * as of this writing idx_of_outcome() will ignore m_linear_bucket_sz_or_0 anyway, given `size() == 2`.) */
88 (n_buckets == 2)
90
91 // Must fill out the bucket-labels (min-value for each bucket), as noted earlier.
92 m_bucket_val0s[0] = bucket0_val0;
93 auto next_val = bucket0_val0 + bucket0_sz;
94 for (size_t idx = 1; idx != n_buckets; ++idx)
95 {
96 m_bucket_val0s[idx] = next_val;
97 next_val += bucket_sz;
98 }
99
100 clear(); // See other ctor for brief explanation.
101} // Histogram_counter::Histogram_counter()
102
104{
105 // As noted in m_per_bucket_counts doc header, the `= default` impl will not compile due to atomic<>, so we gotta:
106 operator=(src);
107}
108
110{
111 using std::vector;
112 using std::atomic;
113
114 // As noted in m_per_bucket_counts doc header, the `= default` impl will not compile due to atomic<>, so we gotta:
115
116 if (&src != this)
117 {
121
122 // Replace our bucket-vector with a freshly-sized one containing whatever. Then manually copy the values.
123 m_per_bucket_counts = vector<atomic<count_t>>(src.size()); // Avoid {}, might list-initialize.
124
125 const auto n_buckets = size();
126 for (size_t idx = 0; idx != n_buckets; ++idx)
127 {
129 }
130 }
131
132 return *this;
133} // Histogram_counter::operator=()
134
136{
137 assert((m_bucket_val0s == src.m_bucket_val0s) && "Dif structures; stop that."); // Vector size + values compare.
138 assert((m_bucket_n_sz == src.m_bucket_n_sz) && "Dif structures; stop that.");
139 assert((m_linear_bucket_sz_or_0 == src.m_linear_bucket_sz_or_0) && "Bug in ctor(s)? This should be invariant.");
140
141 const auto n_buckets = size();
142 for (size_t idx = 0; idx != n_buckets; ++idx)
143 {
145 }
146
147 return *this;
148}
149
151{
152 assert((m_bucket_val0s == src.m_bucket_val0s) && "Dif structures; stop that."); // Vector size + values compare.
153 assert((m_bucket_n_sz == src.m_bucket_n_sz) && "Dif structures; stop that.");
154 assert((m_linear_bucket_sz_or_0 == src.m_linear_bucket_sz_or_0) && "Bug in ctor(s)? This should be invariant.");
155
156 const auto n_buckets = size();
157 for (size_t idx = 0; idx != n_buckets; ++idx)
158 {
160 }
161
162 return *this;
163}
164
166{
167 assert((divisor != 0) && "Divisor must not be zero.");
168
169 const auto n_buckets = size();
170 for (size_t idx = 0; idx != n_buckets; ++idx)
171 {
172 store(&m_per_bucket_counts[idx], load(m_per_bucket_counts[idx]) / divisor);
173 }
174
175 return *this;
176}
177
179{
180 static_assert(static_cast<int32_t>(0xFFFF'FFFFu) == static_cast<int32_t>(-1),
181 "We require two's complement in our reasoning; C++20 guarantees it; below that, no, but in "
182 "reality yes.");
183
184 using std::upper_bound;
185
186 // See m_bucket_val0s doc header for this algorithm's overview; this follows that outline.
187
188 const auto bucket0_val0 = m_bucket_val0s.front();
189 const auto bucket0_sz = m_bucket_val0s[1] - bucket0_val0;
190 if (size() == 2)
191 {
192 // Degenerate case.
193 return (val < (bucket0_val0 + bucket0_sz)) ? 0 : 1;
194 }
195 // else if (size() > 2):
196
198 {
199 // General case: O(log n_buckets) for the binary search.
200 const auto start_it = m_bucket_val0s.begin();
201 const auto idx = upper_bound(start_it, m_bucket_val0s.end(), val) - start_it;
202 return (idx == 0) ? 0 : (idx - 1);
203 }
204 // else if (m_linear_bucket_sz_or_0 != 0): Optimized case: O(1) for some comparisons and arithmetic; no loop.
205
206 if (val <= bucket0_val0)
207 {
208 return 0;
209 /* val below bucket0 min-value => definitely bucket 0 and eliminates worrying about annoying math below.
210 * val equal to it => definitely bucket 0 as well, so while the math below would probably work too, might as well
211 * just return early anyway. */
212 }
213 /* else if (val > bucket0_val0):
214 * We need to know the guaranteed-positive difference `val - bucket0_val0` a/k/a `offset`.
215 * Then we'll just determine which bucket that's in (bucket 0 begins at offset 0 exactly and is of size
216 * bucket0_sz, then after that they're all of size m_linear_bucket_sz_or_0).
217 *
218 * Tempting to just have, say, `auto& offset = val` and calculate it in-place like `offset -= bucket0_val0`,
219 * but this can lead to over/underflow trouble for very high or very low `val`. E.g.,
220 * with `offset = val = max_int` and `bucket0_val0 == -1` => offset = `min_int`, when it should be `max_int + 1`.
221 *
222 * So need to be more careful than that. Call val and bucket0_val0 V and V0 respectively. Firstly
223 * the answer `offset` must be of the unsigned type with the same width as value_t; call this offset_t.
224 * Then it can store the difference between any two V and V0 (with V0 < V) including, e.g.,
225 * `max_int` plus one from the prev paragraph.
226 *
227 * As for how to compute this difference `V - V0`.... If unaccustomed to this topic one can get paranoid.
228 * If `V0 >= 0`, then it's fine to simply do `V - V0` and cast to offset_t; but if `V0 < 0`, then we've got
229 * various overflow worries. It's fine, though; the answer is actually simple:
230 * - cast each of V and V0 to offset_t;
231 * - do the subtraction. The resulting offset_t is correct.
232 *
233 * This works due to 2's complement and standard guarantees. In short that is because the cast from signed to
234 * unsigned, including from a negative number, is formally guaranteed to wrap-around
235 * as needed to yield the correct answer mod(2^N). We omit the proof here. (For example
236 * `(uint)MAX_INT - (uint)(-1) == X`, where `X` is uint and indeed equals MAX_INT plus one, which is indeed
237 * the distance from -1 to MAX_INT.)
238 *
239 * (Without that, we could have used some more complex logic along the lines of: If V0 is non-negative, then
240 * just subtract. If V0 is negative, then carefully calculate `v0 - value_t.min` and `v - value_t.min`, the only
241 * hard case being the latter subtraction, if `v >= 0` -- would have to do some careful casting and negation.
242 * Having gotten those two values -- both non-negative now -- subtract one from the other.) */
243
244 using offset_t = std::make_unsigned_t<value_t>; // Range [0, (value_t.max + 1) * 2).
245 auto offset = static_cast<offset_t>(val) - static_cast<offset_t>(bucket0_val0);
246
247 // `offset` = how far past first-bucket's min-value is val? Recall also `offset >= 1`.
248 const auto bucket0_sz_pos = static_cast<offset_t>(bucket0_sz); // Just to keep signedness consistent/avoid warns.
249 if (offset < bucket0_sz_pos)
250 {
251 return 0; // val > first-bucket's min-value -and- <= first-bucket's max-value.
252 }
253 // else: Bucket 0 is eliminated, so now it's irrelevant, and we ~repeat versus the bucket collection 1, 2, ....
254
255 offset -= bucket0_sz_pos;
256
257 // `offset` now = how far past second-bucket's min-value is val?
258
259 /* We've decremented `offset` by at least 1, so adding 1 cannot overflow. Be careful to not go down to size_t
260 * yet; idx might be gigantic at this point. */
261 const auto idx = static_cast<offset_t>(1) + (offset / static_cast<offset_t>(m_linear_bucket_sz_or_0));
262
263 const auto n_buckets = size();
264 if (idx < static_cast<offset_t>(n_buckets))
265 {
266 return static_cast<size_t>(idx); // Safe given the `if`.
267 }
268 // else if (idx >= n_buckets):
269
270 // By our rules last-bucket grabs everything belonging directly to it -- or beyond it.
271 return n_buckets - 1;
272} // Histogram_counter::idx_of_outcome()
273
275{
276 return m_per_bucket_counts.size();
277}
278
280{
281 assert(idx < size());
282 return load(m_per_bucket_counts[idx]);
283}
284
286{
287 for (auto& per_bucket_count : m_per_bucket_counts)
288 {
289 store(&per_bucket_count, 0);
290 }
291}
292
294{
295 assert(idx < size());
296 store(&m_per_bucket_counts[idx], new_count);
297}
298
299void Histogram_counter::to_ostream(std::ostream* os_ptr) const
300{
301 auto& os = *os_ptr;
302
303 /* E.g.: If n_buckets=4, bucket_sz=5, bucket0_val0=-1, bucket0_sz=3, then output might be:
304 * ..-1..1[3]..6[0]..11[97]..16..[3].
305 * Additionally, any max-length sequence (excluding bucket 0) of 2+ buckets with all-0 counts should be
306 * compacted into: ..{N}..x[0], where x is the normal last-val for the last 0-bucket in the sequence,
307 * and {N} is the length of the 0-sequence, minus one:
308 * ..-1..1[0]..{2}..16..[0] (compacted from ..-1..1[0]..6[0]..11[0]..16..[0])
309 * ..-1..1[0]..{1}..11[0]..16..[3] (compacted from ..-1..1[0]..6[0]..11[0]..16..[3])
310 * ..-1..1[2]..{1}..11[0]..16..[3] (compacted from ..-1..1[2]..6[0]..11[0]..16..[3])
311 *
312 * (That example, for simplicity, was premised on a linear-scale ctor form (hence bucket_sz, bucket0_val0, etc.).
313 * However the output format is the same in general, even if all the bkt widths are different from each other.
314 * All that changes is the sequence of indices (above, -1, 1, 6, 11, 16: even after the first pair -- but
315 * they could just be not, if one uses the different ctor and a funkier sequence of bucket min-values et al.) */
316
317 const auto n_buckets = size();
318
319 /* Due to the look-ahead/skip scheme below, we need to know both idx-th count and `idx+1`th too in the
320 * same iteration; but dupe-load()ing of the same idx, while they can concurrently change, can lead to
321 * bugs. So have to cache it. This starts it off. */
322 value_t next_val = load(m_per_bucket_counts.front());
323
324 size_t skipped0_ct = 0;
325 for (size_t idx = 0; idx != n_buckets; ++idx)
326 {
327 const bool first = idx == 0;
328 const bool last = (idx + 1) == n_buckets;
329 const auto val = next_val;
330 last || (next_val = load(m_per_bucket_counts[idx + 1]));
331 const bool val0 = val == 0;
332
333 /* If count is 0, and the next count is also 0, then skip output of this one.
334 * (Suppose skipped0_ct is 0 (steady state). Then:
335 * If next-ct is 0, but next-next-ct is >0: `..{1}..0` shall be printed upon reaching next-ct.
336 * If next-next-ct is 0 but next-next-next-ct is >0: `..{2}..0` shall be printed upon reaching next-next-ct. Etc.)
337 *
338 * If count is 0, and the next count is not 0, show a count of how many were skipped in a row; then normal output.
339 * If count > 0, then just normal output.
340 *
341 * Exception:
342 * - Don't skip 0 if `first`: Bkt0 normal output looks a bit different, so just always output whatever
343 * is in there for consistency at least; start the 0-skipping (if any) from `idx == 1`.
344 * Tricksy:
345 * - For `last` there is no next count; time to "flush" at that point; so act as-if next count is not 0. */
346 if ((!first) && (!last) && val0 && (next_val == 0))
347 {
348 ++skipped0_ct;
349 }
350 else
351 {
352 if (first)
353 {
354 os << ".." << m_bucket_val0s.front(); // Special prefix for `idx == 0` (indicates underflows + init value).
355 }
356 else
357 {
358 const bool skipped0s = skipped0_ct != 0;
359 assert((val0 || (!skipped0s)) && "This-ct=0 => may or may not have skipped 1+ zeroes before us; "
360 "this-ct>0 => must not have skipped any zeroes before us.");
361 if (skipped0s)
362 {
363 os << "..{" << skipped0_ct << "}"; // "Flush" the skipped 0s in the form of their count.
364 skipped0_ct = 0;
365 }
366 }
367
368 if (last)
369 {
370 /* Print max-value. In this case that is min-value + bkt-width - 1. (There is no "next" min-value.)
371 * Plus add special postfix for last bucket (indicates overflows). */
372 os << ".." << (m_bucket_val0s[idx] + m_bucket_n_sz - 1) << "..";
373 }
374 else
375 {
376 // Print max-value, which is defined as *next* min-value minus 1.
377 os << ".." << (m_bucket_val0s[idx + 1] - 1);
378 }
379
380 os << '[' << val << ']';
381 } // else // if (skip this 0, because the next count is also 0)
382 } // for (idx in [0, n_buckets))
383} // Histogram_counter::to_ostream()
384
386{
387 target_val->clear();
388}
389
390std::ostream& operator<<(std::ostream& os, const Histogram_counter& val)
391{
392 val.to_ostream(&os);
393 return os;
394}
395
396} // 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
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
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
Sub-module of flow::util providing lightweight facilities for working with simple structs of stat cou...
Definition: histo.cpp:26
void fetch_sub(T *target_val, const boost::type_identity_t< T > &subtrahend_val)
Default fetch_sub(&target_stats.m_val, _val) that simply applies *target_val -= subtrahend_val.
Definition: stat_set.hpp:369
const T & load(const T &val)
Default load(stats.m_val) that simply return stats.m_val.
Definition: stat_set.hpp:332
void reset(Histogram_counter *target_val, const Histogram_counter &)
Executes target_val->clear(), where *target_val is a Histogram_counter.
Definition: histo.cpp:385
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
std::ostream & operator<<(std::ostream &os, const Histogram_counter &val)
Prints string representation of the given Histogram_counter to the given ostream.
Definition: histo.cpp:390
void store(T *target_val, const boost::type_identity_t< T > &val)
Default store(&target_stats.m_val, val) that simply assigns *target_val = val.
Definition: stat_set.hpp:344