Flow 2.0.0
Flow project: Full implementation reference.
stat_set.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
24#include <boost/core/noncopyable.hpp>
25#include <ostream>
26#include <atomic>
27#include <cassert>
28#include <string>
29#include <type_traits>
30#include <utility>
31#include <vector>
32
33// Macros.
34
35/**
36 * Declares an individual `Stat_set`-stat-member to be tracked in `flow::util::stat::stats_*()` APIs,
37 * except for stats with Stat_type::S_HI_WMARK. Intended for use inside a `declare_stats()` function body.
38 *
39 * @see FLOW_UTIL_STAT_DECLARE_HI_WMARK() which is used to declare `HI_WMARK`-type stats.
40 *
41 * ### Advanced info ###
42 * This is of interest, we suspect, only if you want to perhaps write your own `stats_*()`-like function for
43 * some custom needs; for example see the doc header for stats_sum() with some relevant discussion.
44 *
45 * The macro expands to a call to `visitor(...)` (as passed-to `declare_stats()`) for the given data member.
46 * `visitor()` shall expect the following args with the following requirements and semantics. In each case
47 * `T` is the type of the `Stat_set` member being processed.
48 * -# `const T* src_val`: The member of an immutable source `const Stat_set* src_stats` -- `src_stats->ARG_m_stat` --
49 * or null if such is not necessary for the `stats_*()` op that's invoking `declare_stats()`, in which case
50 * `src_stats` shall be null to indicate this.
51 * -# `T* target_val`: The member of a mutable target `Stat_set* target_stats` -- `target_stats->ARG_m_stat` --
52 * or null if such is not necessary for the `stats_*()` op that's invoking `declare_stats()`, in which case
53 * `target_stats` shall be null to indicate this.
54 * -# `nullptr`: Ignored here. See FLOW_UTIL_STAT_DECLARE_HI_WMARK().
55 * -# `auto stat_type_tag`: The type (accumulator, gauge, etc.) of the member encoded in such a way as to be able
56 * to determine said type at compile-time via `if constexpr(...stat_type_tag...)`.
57 * It may be ignored if unneeded by the op that's invoking `declare_stats()`. With this macro this can
58 * be any type except `HI_WMARK`.
59 * -# `const Stat_name& name`: The printable name of the member (with `name_prefix` in front). It is computed only on
60 * request -- `name.str()`, `name.to_ostream(os)`, et al -- so if unneeded by the op that's invoking
61 * `declare_stats()`, then ignoring it costs nothing. Do not keep it past the `visitor()` call; see Stat_name.
62 *
63 * When writing the `visitor()` lambda passed to a `stats_*()` op, start with the signature implied by
64 * that list; then for readability consider replacing each ignored arg with `auto&&` (to avoid clutter
65 * and unused-arg warnings or `[[maybe_unused]]`).
66 *
67 * @param ARG_m_stat
68 * The data member to declare, written exactly as the member identifier (e.g., `m_msg_count` or
69 * `m_rcv.m_n_transmitted`).
70 * @param ARG_stat_type
71 * The `X` in `"Stat_type::S_X"` indicating the kind of stat the data member tracks. Note the `Stat_type::S_`
72 * fragment is to be omitted.
73 */
74#define FLOW_UTIL_STAT_DECLARE(ARG_m_stat, ARG_stat_type) \
75 FLOW_UTIL_SEMICOLON_SAFE \
76 ( \
77 using Stat_type = ::flow::util::stat::Stat_type; \
78 using Member_ptr_type = decltype(&target_stats->ARG_m_stat); \
79 constexpr auto FLOW_UTIL_STAT_DECLARE_stat_type = Stat_type::S_##ARG_stat_type; \
80 static_assert(FLOW_UTIL_STAT_DECLARE_stat_type != Stat_type::S_HI_WMARK, \
81 "To declare HI_WMARK stat please use FLOW_UTIL_STAT_DECLARE_HI_WMARK()."); \
82 visitor(src_stats ? &src_stats->ARG_m_stat : nullptr, \
83 target_stats ? &target_stats->ARG_m_stat : nullptr, \
84 static_cast<Member_ptr_type>(nullptr), \
85 std::integral_constant<Stat_type, FLOW_UTIL_STAT_DECLARE_stat_type>{}, \
86 ::flow::util::stat::Stat_name{name_prefix, #ARG_m_stat, \
87 ::flow::util::stat::Stat_name::Fragment_kind::S_MEMBER_ID}); \
88 )
89
90/**
91 * Declares an individual `Stat_set`-stat-member of Stat_type::S_HI_WMARK to be tracked
92 * in `flow::util::stat::stats_*()` APIs. Intended for use inside a `declare_stats()` function body.
93 *
94 * @see FLOW_UTIL_STAT_DECLARE() which is used to declare non-`HI_WMARK`-type stats.
95 *
96 * ### Conventions (recommended) ###
97 * Convention (order): FLOW_UTIL_STAT_DECLARE() for `ARG_m_gauged_stat` should immediately precede
98 * this FLOW_UTIL_STAT_DECLARE_HI_WMARK() for `ARG_m_stat`.
99 *
100 * Convention (naming): Name the member `<gauged_thing>_hi_wmark`; e.g., `m_n_open_pools` is tracked
101 * by `m_n_open_pools_hi_wmark`.
102 *
103 * ### Advanced info ###
104 * This is of interest, we suspect, only if you want to perhaps write your own `stats_*()`-like function for
105 * some custom needs; for example see the doc header for stats_sum() with some relevant discussion.
106 *
107 * Expands to a call to `visitor(...)` for the given data member.
108 * `visitor()` shall expect the following args with the following requirements and semantics. In each case
109 * `T` is the type of the `Stat_set` member being processed.
110 * -# `const T* src_val`: See FLOW_UTIL_STAT_DECLARE().
111 * -# `T* target_val`: See FLOW_UTIL_STAT_DECLARE().
112 * -# `const T* hi_wmark_gauged_target_val`: Member of a mutable target `Stat_set* target_stats` --
113 * `target_stats->ARG_m_gauged_stat` -- or null if such is not necessary for the `stats_*()` op
114 * that's invoking `declare_stats()`, in which case `target_stats` shall be null to indicate this.
115 * -# `auto stat_type_tag`: See FLOW_UTIL_STAT_DECLARE().
116 * -# `const Stat_name& name`: See FLOW_UTIL_STAT_DECLARE().
117 *
118 * When writing the `visitor()` lambda... (same as FLOW_UTIL_STAT_DECLARE() doc header says).
119 *
120 * @param ARG_m_stat
121 * The `HI_WMARK`-type data member to declare, written exactly as the member identifier (e.g., `m_msg_count` or
122 * `m_rcv.m_n_transmitted`).
123 * @param ARG_m_gauged_stat
124 * The `GAUGE`-type data member that is being tracked by the high-water-mark `ARG_m_stat`.
125 */
126#define FLOW_UTIL_STAT_DECLARE_HI_WMARK(ARG_m_stat, ARG_m_gauged_stat) \
127 FLOW_UTIL_SEMICOLON_SAFE \
128 ( \
129 using Stat_type = ::flow::util::stat::Stat_type; \
130 static_assert(std::is_same_v<decltype(&target_stats->ARG_m_stat), \
131 decltype(&target_stats->ARG_m_gauged_stat)>, \
132 "The HI_WMARK stat must be the same C++ type as the GAUGEd stat."); \
133 visitor(src_stats ? &src_stats->ARG_m_stat : nullptr, \
134 target_stats ? &target_stats->ARG_m_stat : nullptr, \
135 target_stats ? &target_stats->ARG_m_gauged_stat : nullptr, \
136 std::integral_constant<Stat_type, Stat_type::S_HI_WMARK>{}, \
137 ::flow::util::stat::Stat_name{name_prefix, #ARG_m_stat, \
138 ::flow::util::stat::Stat_name::Fragment_kind::S_MEMBER_ID}); \
139 )
140
141namespace flow::util::stat
142{
143
144// Types.
145
146/**
147 * Thin proxy wrapping a `const` reference to a `Stat_set`, enabling `ostream << stat::print(my_stats)` syntax.
148 * Obtain one via print().
149 *
150 * @see print()
151 *
152 * @tparam Stat_set
153 * See stats_to_ostream() for requirements.
154 */
155template<typename Stat_set>
157{
158 // Data.
159
160 /// The referenced stat set.
161 const Stat_set& m_stats;
162};
163
164/**
165 * Specialized class that (at a light weight) represents a printable `Stat_set`-member name, or fragment thereof,
166 * by storing concatenated sub-fragments in rope-like fashion, so that certain relatively expensive yet rarely needed
167 * operations (given that stat-member names are used only for things like pretty-printing) are executed only
168 * when actually needed.
169 *
170 * ### How to use ###
171 * Typically you'll touch Stat_name only when writing a `declare_stats()` that composes `declare_stats()`s of
172 * sub-`struct`s, optionally adding a sub-prefix for those sub-`struct`s' stat names. As an advanced user
173 * you might also write your own `stats_*()`-like op. Thus:
174 *
175 * ~~~
176 * // A stats_*() op calls the top-level declare_stats() with the empty root Stat_name.
177 * declare_stats("", &src_stats, target_stats, visitor);
178 *
179 * template<typename Visitor>
180 * void declare_stats(const Stat_name& name_prefix, const My_stats* src_stats, My_stats* target_stats,
181 * Visitor&& visitor)
182 * {
183 * // Plain fields: just declare them; name_prefix is applied automatically.
184 * FLOW_UTIL_STAT_DECLARE(m_msg_count, ACCUMULATOR);
185 *
186 * // Sub-struct, same prefix: forward name_prefix as-is.
187 * declare_stats(name_prefix,
188 * src_stats ? &src_stats->m_misc : nullptr, target_stats ? &target_stats->m_misc : nullptr,
189 * visitor);
190 *
191 * // Sub-struct, extended prefix: names come out as "<prefix>snd.<field>".
192 * declare_stats(Stat_name{name_prefix, "snd."},
193 * src_stats ? &src_stats->m_snd : nullptr, target_stats ? &target_stats->m_snd : nullptr,
194 * visitor);
195 *
196 * // Several sub-structs under a shared extended prefix: name it once; reuse it (as-is or extended further).
197 * const Stat_name obj_prefix{name_prefix, "obj."};
198 * declare_stats(Stat_name{obj_prefix, "own."}, ..., visitor);
199 * declare_stats(Stat_name{obj_prefix, "lnd."}, ..., visitor);
200 * declare_stats(obj_prefix, ..., visitor);
201 * }
202 * ~~~
203 *
204 * Inside a `visitor()` (a custom `stats_*()` op): take the name arg as `const Stat_name& name` if you need it -- then
205 * `name.str()` (or `name.append_to(&some_string)`, or `name.to_ostream(os)`) -- or else as `auto&&` and ignore it
206 * (which costs nothing).
207 *
208 * ### Rationale ###
209 * We gave a mini-rationale above. To set it up more concretely: `stats_*()` ops routinely use ADL-determined
210 * `declare_stats()` functions (which call `FLOW_UTIL_STAT_DECLARE*()` and sometimes compose via further
211 * `declare_stats()` calls) given by the user to perform work for each stat-member of the given `Stat_set`.
212 * A `visitor()` thus visits (through those `DECLARE`s) each stat-member, the macro passing-in various info
213 * about that stat-member, such as its address in a source `Stat_set` and/or in a target one. One of the
214 * items is the stat-member's printable name as a string, computed via cfg::value_set_member_id_to_opt_name()
215 * as called on the stringification of the member's identifier.
216 *
217 * However: most `stats_*()` guys will disregard this name; while a couple existing ones (stats_to_ostream(),
218 * usually invoked via print(), especially) will care about it very much. Yet `FLOW_UTIL_STAT_DECLARE*()`
219 * would naively compute this name anyway -- per member! -- regardless of whether it's ignored, which it usually is.
220 * It is wasteful. / So instead we could store the argument *to* the `value_set_member_id_to_opt_name()` in
221 * a `*this` (as a mere `String_view`), and invoke that function only when a `stats_*()` actually requests the
222 * overall member-name *string* to be *computed*.
223 *
224 * A secondary but basically similar driver is when a user `declare_stats()` might use *composition*
225 * (see util::stat doc header). For nice stat-member names to work `declare_stats()` takes a string name-prefix,
226 * which is prepended to the aforementioned basic member-identifier name from the macro. So if `declare_stats()`
227 * is called inside `declare_stats()` for a sub-`struct` in the `Stat_set` (composition), it might pass-through
228 * not that same name-prefix but rather it plus a string literal IDing the sub-`struct`. Then the sub-`struct`'s
229 * member `m_cool_val` would come out to, like, "prefix_given_to_us.sub_struct_prefix_we_added.cool_val".
230 * So with composition, naively, the prefix concatenation is computed once per such call -- but, again, it
231 * is only needed when the `visitor()` even cares about names at all.
232 *
233 * So there you have it: store a prefix-base (if any) (itself a Stat_name for this to work), an actual fragment-string
234 * (as a `String_view`), and whether (when computing actual resulting string as in str()) to just concatenate
235 * the two or to call cfg::value_set_member_id_to_opt_name() first. The computation is done only on-demand;
236 * and the storage is of merely ~2 pointers, a length, and a flag.
237 */
238class Stat_name : private boost::noncopyable
239{
240public:
241 // Types.
242
243 /// How a fragment of text is rendered into the name.
244 enum class Fragment_kind
245 {
246 /// The fragment is rendered verbatim.
247 S_LITERAL,
248
249 /**
250 * The fragment is a data member identifier (e.g., `m_msg_count` or `m_rcv.m_n_transmitted`) and is rendered as
251 * its printable stat name via cfg::value_set_member_id_to_opt_name(). Used by
252 * FLOW_UTIL_STAT_DECLARE() and FLOW_UTIL_STAT_DECLARE_HI_WMARK(); you are unlikely to need it directly.
253 */
254 S_MEMBER_ID
255 }; // enum class Fragment_kind
256
257 // Constructors/destructor.
258
259 /**
260 * Constructs a Stat_name that represents only a name fragment with no reference to another Stat_name.
261 *
262 * @param fragment
263 * The text; usually a literal or nothing. It is referred-to, not copied; see class doc header for use-cases.
264 */
265 Stat_name(String_view fragment = {});
266
267 /**
268 * Identical to first ctor but takes `const char*` (usually a literal). Exists because an implicit conversion
269 * from a string literal to `String_view` and then to Stat_name would be two user-defined conversions in a row,
270 * which C++ does not do implicitly.
271 *
272 * @param fragment
273 * See other ctor.
274 */
275 Stat_name(const char* fragment);
276
277 /**
278 * Constructs a Stat_name that represents the concatenation of another Stat_name and the specified name fragment (or
279 * `value_set_member_id_to_opt_name()`-ification thereof).
280 *
281 * @param base
282 * First part of the concatenation. It is referred-to, not copied; see class doc header for use-cases.
283 * @param fragment
284 * Second part of the concatenation, interpreted according to `fragment_kind`.
285 * @param fragment_kind
286 * How to (on-demand only) render `fragment` into a string via (at least) str().
287 */
288 Stat_name(const Stat_name& base, String_view fragment, Fragment_kind fragment_kind = Fragment_kind::S_LITERAL);
289
290 // Methods.
291
292 /**
293 * On-demand evaluation: `*target += this->str()` but faster.
294 *
295 * @param target
296 * The string to which to append.
297 */
298 void append_to(std::string* target) const;
299
300 /**
301 * On-demand evaluation: `os << this->str()` but faster.
302 *
303 * @param os
304 * The stream to which to write.
305 */
306 void to_ostream(std::ostream& os) const;
307
308 /**
309 * On-demand evaluation: Returns the full name -- all fragments, from the root on, each rendered per its
310 * Fragment_kind -- as a new string. Equivalent to append_to() into an empty string.
311 *
312 * @return See above.
313 */
314 std::string str() const;
315
316private:
317 // Data.
318
319 /// First part, if any, of the concatenation in str().
320 const Stat_name* const m_base;
321
322 /// Second part, if any, of the concatenation in str(), interpreted according to #m_fragment_kind.
324
325 /// How to render #m_fragment.
327}; // class Stat_name
328
329// Template implementations.
330
331template<typename T>
332const T& load(const T& val)
333{
334 return val;
335}
336
337template<typename T>
338T load(const std::atomic<T>& val)
339{
340 return val.load(std::memory_order_relaxed);
341}
342
343template<typename T>
344void store(T* target_val, const boost::type_identity_t<T>& val)
345{
346 *target_val = val;
347}
348
349template<typename T>
350void store(std::atomic<T>* target_val, boost::type_identity_t<T> val)
351{
352 target_val->store(val, std::memory_order_relaxed);
353}
354
355template<typename T>
356void fetch_add(T* target_val, // Yes, intentionally void. See doc header.
357 const boost::type_identity_t<T>& addend_val)
358{
359 *target_val += addend_val;
360}
361
362template<typename T>
363T fetch_add(std::atomic<T>* target_val, boost::type_identity_t<T> addend_val)
364{
365 return target_val->fetch_add(addend_val, std::memory_order_relaxed);
366}
367
368template<typename T>
369void fetch_sub(T* target_val, // Yes, intentionally void. See doc header.
370 const boost::type_identity_t<T>& subtrahend_val)
371{
372 *target_val -= subtrahend_val;
373}
374
375template<typename T>
376T fetch_sub(std::atomic<T>* target_val, boost::type_identity_t<T> subtrahend_val)
377{
378 return target_val->fetch_sub(subtrahend_val, std::memory_order_relaxed);
379}
380
381template<typename T>
382T exchange(std::atomic<T>* target_val, boost::type_identity_t<T> val)
383{
384 return target_val->exchange(val, std::memory_order_relaxed);
385}
386
387template<typename T>
388void update_hi_wmark(T* target_val, const boost::type_identity_t<T>& val)
389{
390 if (*target_val < val)
391 {
392 *target_val = val;
393 }
394}
395
396template<typename T>
397void update_hi_wmark(std::atomic<T>* target_val, boost::type_identity_t<T> val)
398{
399 T cur = load(*target_val);
400 while ((cur < val) // Over time this is probably false immediately, and we therefore typically no-op.
401 && (!target_val->compare_exchange_weak(cur, val, std::memory_order_relaxed, std::memory_order_relaxed)))
402 {
403 // CAS failed: another thread bumped *target_val; `cur` is now its new value -- re-check `cur < val`.
404 }
405}
406
407template<typename T>
408void reset(T* target_val, const T& fresh_val)
409{
410 store(target_val, load(fresh_val)); // Exactly as advertised.
411}
412
413template<typename Stat_set>
414void stats_to_ostream(std::ostream& os, const Stat_set& stats)
415{
416 bool first = true;
417 declare_stats("", &stats, nullptr,
418 [&](const auto* val_ptr, auto&&, auto&&, auto&&, const Stat_name& name)
419 {
420 const auto& val = *val_ptr;
421
422 if (!first)
423 {
424 os << ' ';
425 }
426 first = false;
427
428 // In particular load(val) efficiently loads/returns copy of atomic<> val; simply returns regular val by value.
429 name.to_ostream(os);
430 os << "=[" << load(val) << ']';
431 }); // declare_stats()
432 // ADL finds the overload of declare_stats() matching Stat_set.
433}
434
435template<typename Stat_set>
437{
438 return { stats };
439}
440
441template<typename Stat_set, typename... Ctor_args>
442std::vector<std::string> stats_field_names(Ctor_args&&... ctor_args)
443{
444 using std::vector;
445 using std::string;
446
447 vector<string> names;
448 // Typically -- not always -- just `()`. / Avoid initializer-list pitfalls in this generic code: (), not {} here.
449 const Stat_set dummy(std::forward<Ctor_args>(ctor_args)...);
450 declare_stats("", &dummy, nullptr,
451 [&](auto&&, auto&&, auto&&, auto&&, const Stat_name& name)
452 {
453 names.emplace_back(name.str());
454 }); // declare_stats()
455
456 return names;
457}
458
459template<typename Stat_set>
460std::ostream& operator<<(std::ostream& os, const Stat_set_printable<Stat_set>& printable)
461{
462 stats_to_ostream(os, printable.m_stats);
463 return os;
464}
465
466template<typename Stat_set>
467void stats_reset(Stat_set* target_stats, const Stat_set& fresh_stats)
468{
469 declare_stats("", &fresh_stats, target_stats,
470 [&]([[maybe_unused]] const auto* fresh_val_ptr, [[maybe_unused]] auto* target_val,
471 [[maybe_unused]] const auto* hi_wmark_gauged_target_val_or_null, auto stat_type_tag, auto&&)
472 {
473 constexpr auto stat_type = decltype(stat_type_tag)::value;
474
475 if constexpr(stat_type == Stat_type::S_ACCUMULATOR)
476 {
477 /* By ACCUMULATOR doc header rules:
478 * "Make it so that `target_stats->m_v` is as-if one just performed:
479 * `store(&target_stats->m_v, load(fresh_stats.m_v));`."
480 * If further explains that (1) reset() does this; and (2) user may override it by defining an
481 * ADL-findable compatible reset(). (E.g., Histogram_counter does this; namely it does target_val->clear().) */
482 reset(target_val, *fresh_val_ptr);
483 }
484 // else if (GAUGE) // By GAUGE doc header rules: no-op. The measured state is the measured state.
485 else if constexpr(stat_type == Stat_type::S_HI_WMARK)
486 {
487 /* By HI_WMARK doc header rules: copy the GAUGEd stat. New measurement period => current = highest.
488 * (The gauged value in *target_stats is sound to read regardless of declare-order: a reset is a
489 * no-op for GAUGEs, so no pass of ours modifies it.) */
490 store(target_val, load(*hi_wmark_gauged_target_val_or_null));
491 }
492 }); // declare_stats()
493} // stats_reset()
494
495template<typename Stat_set>
496void stats_assign(Stat_set* target_stats, const Stat_set& src_stats)
497{
498 declare_stats("", &src_stats, target_stats,
499 [&](const auto* src_val_ptr, auto* target_val, auto&&, auto&&, auto&&)
500 {
501 store(target_val, load(*src_val_ptr));
502 /* Sanity-check for the case where T=Histogram_counter: This reduces to `*target_val = *src_val_ptr`,
503 * which for each per-bucket counter reduces to store(&target_counter, load(&src_counter)). */
504 });
505} // stats_assign()
506
507template<typename Stat_set>
508void stats_aggregate_one(Stat_set* target_stats, const Stat_set& src_stats)
509{
510 declare_stats("", &src_stats, target_stats,
511 [&](const auto* src_val_ptr, auto* target_val, auto&&, auto stat_type_tag, auto&&)
512 {
513 constexpr auto stat_type = decltype(stat_type_tag)::value;
514 const auto& src_val = *src_val_ptr;
515
516 if constexpr((stat_type == Stat_type::S_ACCUMULATOR) || (stat_type == Stat_type::S_GAUGE))
517 {
518 // By these two's doc header rules, `a += b` is how to aggregate b into a.
519 fetch_add(target_val, load(src_val)); // Type must be `atomic<T_numeric>` or have a reasonable +=.
520 }
521 else if constexpr(stat_type == Stat_type::S_HI_WMARK)
522 {
523 // By HI_WMARK doc header rules: essentially take the higher of the two high-water mark values.
524 update_hi_wmark(target_val, load(src_val)); // Type must be `atomic<T_numeric>` or have a reasonable <.
525 }
526 }); // declare_stats()
527} // stats_aggregate_one()
528
529template<typename Stat_set, typename It>
530void stats_aggregate(Stat_set* target_stats, const It& src_stats_begin, const It& src_stats_end)
531{
532 assert((src_stats_begin != src_stats_end) && "Must aggregate at least 1 thing.");
533
534 stats_assign(target_stats, *src_stats_begin);
535 auto src_stats_it = src_stats_begin;
536
537 size_t n = 1;
538 for (++src_stats_it; src_stats_it != src_stats_end; ++src_stats_it)
539 {
540 stats_aggregate_one(target_stats, *src_stats_it);
541 ++n;
542 }
543
544 declare_stats("", nullptr, target_stats,
545 [&](auto&&, [[maybe_unused]] auto* target_val, auto&&, auto stat_type_tag, auto&&)
546 {
547 if constexpr(decltype(stat_type_tag)::value == Stat_type::S_GAUGE)
548 {
549 auto scaled_val = load(*target_val);
550 scaled_val /= n; // Type must be `atomic<T_numeric>` or have a reasonable /=.
551
552 store(target_val, scaled_val);
553 }
554 // else { ACCUMULATORs are already summed; HI_WMARKs = max has been found. Nothing to do. }
555 }); // declare_stats()
556} // stats_aggregate()
557
558template<typename Stat_set, typename It>
559void stats_sum(Stat_set* target_stats, const It& src_stats_begin, const It& src_stats_end)
560{
561 assert((src_stats_begin != src_stats_end) && "Must sum at least 1 thing.");
562
563 stats_assign(target_stats, *src_stats_begin);
564 auto src_stats_it = src_stats_begin;
565
566 for (++src_stats_it; src_stats_it != src_stats_end; ++src_stats_it)
567 {
568 declare_stats("", &*src_stats_it, target_stats,
569 [&](const auto* src_val_ptr, auto* target_val, auto&&, auto&&, auto&&)
570 {
571 fetch_add(target_val, load(*src_val_ptr)); // Type must be `atomic<T_numeric>` or have a reasonable +=.
572 });
573 }
574} // stats_sum()
575
576template<typename Stat_set, typename It>
577void stats_aggregate_shards(Stat_set* target_stats, const It& src_stats_begin, const It& src_stats_end,
578 const Stat_set* fresh_stats_from_0_shards)
579{
580 const bool empty_range = src_stats_begin == src_stats_end;
581 assert(((!empty_range) || fresh_stats_from_0_shards) && "Must provide a fresh_stats if no input-shards.");
582
583 /* The algorithm consists of two parts:
584 * -I- Deal with ACC[UMULATOR]s and GAUGEs.
585 * -II- Deal with HWMs (HI_WMARKs).
586 * Discussing -II- first, as it's simplest arguably. "Deal with HWMs" simply means: set each HWM to its
587 * correct value in *target_stats. To wit:
588 * Set HWM to max(HWM already in *target_stats, the current GAUGEd value).
589 * (By definition of HWM: max seen since last reset; by induction that is the higher of last-recorded HWM
590 * and current GAUGEd value.)
591 *
592 * Easy enough; but note it assumes that by the end of -I- *target_stats contains current GAUGE values.
593 * Indeed it will.
594 *
595 * As for -I-: Let's methodically divide it up. We've got ACCs and GAUGEs.
596 *
597 * ACCs: Consider ACC m_x. target_stats->m_x shall be the aggregation (basically sum) of S.m_x, over
598 * all S in the given range.
599 * - Degenerate case: empty_range: There are no shards over which to sum, so as usual leverage
600 * fresh_stats (as in stats_reset()) as the source of zeroes, so to speak. (Here fresh_stats is
601 * *fresh_stats_from_0_shards; and as assert()ed above that is a valid dereference.)
602 * GAUGEs: Consider GAUGE m_x -- the answer is, same as for ACC m_x: sum it or use fresh_stats.
603 * - Note this will finalize target_stats->m_x as required for II. */
604
605 // Let's do -I-: deal with ACCUMULATORs and GAUGEs. This part depends on whether empty_range is true.
606 if (empty_range)
607 {
608 // Degenerate case: just grab 'em from *fresh_stats_from_0_shards.
609 declare_stats("", fresh_stats_from_0_shards, target_stats,
610 [&]([[maybe_unused]] const auto* fresh_val_ptr, [[maybe_unused]] auto* target_val,
611 auto&&, auto stat_type_tag, auto&&)
612 {
613 constexpr auto stat_type = decltype(stat_type_tag)::value;
614 if constexpr((stat_type == Stat_type::S_ACCUMULATOR) || (stat_type == Stat_type::S_GAUGE))
615 {
616 reset(target_val, *fresh_val_ptr);
617 }
618 });
619 } // if (empty_range)
620 else // if (!empty_range)
621 {
622 /* Non-degenerate case: run through the range.
623 *
624 * First, as in stats_aggregate(), copy first src_ thing in range onto *target_stats (then we'll sum the
625 * others onto it, similarly) -- but don't touch HI_WMARKs if any. See below as to why. */
626 auto src_stats_it = src_stats_begin;
627 declare_stats("", &*src_stats_it, target_stats,
628 [&]([[maybe_unused]] const auto* src_val_ptr, [[maybe_unused]] auto* target_val, auto&&,
629 auto stat_type_tag, auto&&)
630 {
631 constexpr auto stat_type = decltype(stat_type_tag)::value;
632 if constexpr((stat_type == Stat_type::S_ACCUMULATOR) || (stat_type == Stat_type::S_GAUGE))
633 {
634 store(target_val, load(*src_val_ptr));
635 }
636 /* else if (HI_WMARK)
637 * { Leave *target_stats alone w/r/t this field! It contains an in-value *and* will store an out-value.
638 * We'll do the update when ready below. Until then we can't lose the in-value. } */
639 }); // declare_stats()
640
641 /* Next, also as in stats_aggregate(), sum the src_ guys past the one already in *target_stats onto
642 * the latter (keeping comments light; see stats_aggregate_one()).
643 * However, as advertised, HI_WMARK is to be treated specially. See below as to how.
644 *
645 * As advertised -- it may be important for the reason stated in the doc header @note to this effect --
646 * we only walk this range exactly once (step 1 just above, steps 2, ..., N just below). */
647 for (++src_stats_it; src_stats_it != src_stats_end; ++src_stats_it)
648 {
649 declare_stats("", &*src_stats_it, target_stats,
650 [&]([[maybe_unused]] const auto* src_val_ptr, [[maybe_unused]] auto* target_val, auto&&,
651 auto stat_type_tag, auto&&)
652 {
653 constexpr auto stat_type = decltype(stat_type_tag)::value;
654 if constexpr((stat_type == Stat_type::S_ACCUMULATOR) || (stat_type == Stat_type::S_GAUGE))
655 {
656 fetch_add(target_val, load(*src_val_ptr));
657 }
658 /* else if (HI_WMARK):
659 * { Tempting to `update_hi_wmark(target_val, ...)` but that's wrong: Need to loop
660 * through the entire range [..., src_stats_end) to get the GAUGEd value fully summed, first,
661 * and only then is it safe to finally (possibly) overwrite the HI_WMARK. } */
662 }); // declare_stats()
663 } // for (src_stats_it in [src_stats_begin + 1, src_stats_end))
664 } // else // if (!empty_range)
665
666 /* Let's do -II-: Now we can finish up the HI_WMARKs.
667 * As advertised: ensure for each HI_WMARK it is = highest of the GAUGEd value's states sampled across
668 * stats_aggregate_shards() calls like ours, including ours. */
669 declare_stats("", nullptr, target_stats,
670 [&](auto&&, [[maybe_unused]] auto* target_val,
671 [[maybe_unused]] const auto* hi_wmark_gauged_target_val_or_null, auto stat_type_tag, auto&&)
672 {
673 if constexpr(decltype(stat_type_tag)::value == Stat_type::S_HI_WMARK)
674 {
675 update_hi_wmark(target_val, load(*hi_wmark_gauged_target_val_or_null));
676 }
677 });
678} // stats_aggregate_shards()
679
680template<typename Stat_set, typename It>
681void stats_reset_shard_aggregate(Stat_set* target_stats, const It& src_stats_begin, const It& src_stats_end,
682 const Stat_set& fresh_stats_from_0_shards)
683{
684 const bool empty_range = src_stats_begin == src_stats_end;
685
686 /* As in stats_aggregate_shards() the algorithm consists of two parts:
687 * -I- Deal with ACC[UMULATOR]s and GAUGEs.
688 * -II- Deal with HWMs (HI_WMARKs).
689 * Discussing -II- first, as it's simplest arguably. "Deal with HWMs" simply means: set each HWM to its
690 * correct value in *target_stats. To wit:
691 * Set HWM = the current GAUGEd value. (By definition of HWM: max seen since last reset... which is now.)
692 *
693 * Easy enough; but note it assumes that by the end of -I- *target_stats contains current GAUGE values.
694 * Indeed it will.
695 *
696 * As for -I-: Let's methodically divide it up. We've got ACCs and GAUGEs.
697 *
698 * ACCs: Consider ACC m_x.
699 * - target_stats->m_x: Does not matter. *target_stats is not being stat-consumed; its whole purpose is
700 * to ultimately end up with the correct HWMs, so that the next stat-consumption inductively can compute same.
701 * So: No-op for target_stats->m_x.
702 * - For each shard S in range: reset(&S.m_x, <from fresh_stats_from_0_shards>). *target_stats is not being
703 * stat-consumed now, but when it does occur the ACC m_x will have been reset (and possibly further
704 * incremented again as normal).
705 * - Degenerate case: empty_range: Nothing to do. No shards to reset -- shards that appear before
706 * the next stat-consumption will be initially fresh.
707 * GAUGEs: Consider GAUGE m_x.
708 * - For each shard S in range: No-op w/r/t S.m_x. By definition of GAUGE, a reset has no effect on it.
709 * - Degenerate case: empty_range: Can't get more degenerate than no-op. Still no-op.
710 * - target_stats->m_x: In and of itself, does not matter: Next stat-consumption will just overwrite it.
711 * However (!): remember the prerequisite for -II- above: target_stats->m_x must be computed as-if
712 * being stat-consumed (so that -II- can then assign the corresponding HWM member, if any, to this value).
713 * (There's a bit of an inefficiency, in that this is a waste for a GAUGE without an HWM. @todo Maybe revisit.)
714 * - Non-degenerate case: See stats_aggregate_shards(). Namely what it does is: Sum it.
715 * - Degenerate case: empty_range: See <ditto>. Namely what it does is: Grab it from
716 * fresh_stats_from_0_shards. */
717
718 if (empty_range)
719 {
720 // See above for reference. What that reduces to is simply reset each GAUGE with help of fresh_stats_from_0_shards.
721 declare_stats("", &fresh_stats_from_0_shards, target_stats,
722 [&]([[maybe_unused]] const auto* fresh_val_ptr, [[maybe_unused]] auto* target_val, auto&&,
723 auto stat_type_tag, auto&&)
724 {
725 if constexpr(decltype(stat_type_tag)::value == Stat_type::S_GAUGE)
726 {
727 reset(target_val, *fresh_val_ptr);
728 }
729 });
730 } // if (empty_range)
731 else // if (!empty_range)
732 {
733 /* See above for reference. Essentially it's the non-degenerate branch of -I- in stats_aggregate_shards()
734 * but limited to GAUGEs only; while for ACCs we reset() each one. Keeping comments light: refer to
735 * stats_aggregate_shards() if needed; but note how ACCs are handled differently.
736 *
737 * Key thing: As promised: a single walk through the range. */
738
739 auto src_stats_it = src_stats_begin;
740
741 const auto reset_shard_accs = [&]() // Little helper.
742 {
743 declare_stats("", &fresh_stats_from_0_shards, &*src_stats_it,
744 [&]([[maybe_unused]] const auto* fresh_val_ptr, [[maybe_unused]] auto* target_shard_val, auto&&,
745 auto stat_type_tag, auto&&)
746 {
747 if constexpr(decltype(stat_type_tag)::value == Stat_type::S_ACCUMULATOR)
748 {
749 reset(target_shard_val, *fresh_val_ptr);
750 }
751 });
752 };
753
754 // Plop shard 1 onto *target_stats (again: GAUGEs only).
755 declare_stats("", &*src_stats_it, target_stats,
756 [&]([[maybe_unused]] const auto* src_val_ptr, [[maybe_unused]] auto* target_val, auto&&,
757 auto stat_type_tag, auto&&)
758 {
759 if constexpr(decltype(stat_type_tag)::value == Stat_type::S_GAUGE)
760 {
761 store(target_val, load(*src_val_ptr));
762 }
763 });
764 reset_shard_accs(); // ...and reset shard 1's ACCs.
765
766 // Sum shards 2, 3, ... onto *target_stats (again: GAUGEs only).
767 for (++src_stats_it; src_stats_it != src_stats_end; ++src_stats_it)
768 {
769 declare_stats("", &*src_stats_it, target_stats,
770 [&]([[maybe_unused]] const auto* src_val_ptr, [[maybe_unused]] auto* target_val, auto&&,
771 auto stat_type_tag, auto&&)
772 {
773 if constexpr(decltype(stat_type_tag)::value == Stat_type::S_GAUGE)
774 {
775 fetch_add(target_val, load(*src_val_ptr));
776 }
777 });
778 reset_shard_accs(); // ...and reset shard N's ACCs.
779 } // for (src_stats_it in [src_stats_begin + 1, src_stats_end))
780 } // else // if (!empty_range)
781
782 // Let's do -II-: Now we can finish up the HI_WMARKs.
783 declare_stats("", nullptr, target_stats,
784 [&](auto&&, [[maybe_unused]] auto* target_val,
785 [[maybe_unused]] const auto* hi_wmark_gauged_target_val_or_null, auto stat_type_tag, auto&&)
786 {
787 if constexpr(decltype(stat_type_tag)::value == Stat_type::S_HI_WMARK)
788 {
789 store(target_val, load(*hi_wmark_gauged_target_val_or_null));
790 }
791 });
792} // stats_reset_shard_aggregate()
793
794template<typename Stat_set>
795void stats_since_reset_state(Stat_set* target_stats, Stat_set* reset_state)
796{
797 declare_stats("", reset_state, target_stats,
798 [&]([[maybe_unused]] const auto* reset_val_ptr, [[maybe_unused]] auto* target_val,
799 [[maybe_unused]] const auto* hi_wmark_gauged_target_val_or_null, auto stat_type_tag, auto&&)
800 {
801 constexpr auto stat_type = decltype(stat_type_tag)::value;
802 const auto& reset_val = *reset_val_ptr;
803
804 if constexpr(stat_type == Stat_type::S_ACCUMULATOR)
805 {
806 // Accumulator: The relative value = current raw value minus reference raw value.
807 fetch_sub(target_val, reset_val); // Note that for non-`atomic`s this means it must have `-=`.
808 /* Sanity-check for the case where T=Histogram_counter: This reduces to `*target_val -= reset_val`,
809 * which for each per-bucket counter reduces to fetch_sub(&target_counter, load(&reset_counter)). */
810 }
811 // else if (GAUGE) // By GAUGE rules: no-op. The measured state is the measured state.
812 else if constexpr(stat_type == Stat_type::S_HI_WMARK)
813 {
814 /* As for the HI_WMARK: Per doc header: *target_val is currently garbage and should be set either to
815 * highest GAUGE's value seen since reset; which by induction is either the HI_WMARK recorded in
816 * reset_val already, or the new just-observed GAUGE's value -- if that is higher still. To keep the
817 * induction going for next time, in the latter case also update reset_val.
818 *
819 * That last sentence... we were so very close to being able to simply do `store(reset_val_ptr, new_gauge)`
820 * below, in the case where `new_gauge < cur_hwm` is false (so, turn the ?: into `if/else` and add that
821 * store() to else{}). Unfortunately *reset_val_ptr is a `const T&`, not a `T&`, so ya can't store() in it;
822 * const-violation. Truth be told, it is defeatable with a const_cast<>, and that is maybe defensible --
823 * we'd be abusing declare_stats()'s official signature, but it's essentially our own signature -- but I
824 * (ygoldfel) just can't make myself do it. So let's do a final separate pass to update these HI_WMARKs
825 * in *reset_state. It's slower (very unlikely that matters in this context) but cleaner. (The alternative
826 * is to expand Visitor signature to cover another op-configuration (0-2 outputs instead of 0-1 of them)
827 * which is probably decent but subjectively getting overly hairy for what one pass can officially do.) */
828 const auto new_gauge = load(*hi_wmark_gauged_target_val_or_null);
829 const auto cur_hwm = load(reset_val);
830
831 store(target_val, (new_gauge < cur_hwm) ? cur_hwm
832 : new_gauge); // (<-- aforementioned would-be else{}.)
833 /* ^-- Instead of < and store()/load()ing could also use something update_hi_wmark()-y; but expressiveness
834 * is probably better this way. (Either way `<` is required.) Also the above+below is multi-step;
835 * we don't even wish to contemplate how it behaves under concurrent modification and therefore advertise
836 * as resulting in undefined-behavior in such scenarios. */
837 } // else if (HI_WMARK)
838 }); // declare_stats()
839
840 // OK, so since we decided not to abuse the system by updating *reset_state HIGH_WMARKs above, do it in separate pass.
841 declare_stats("", target_stats, reset_state,
842 [&]([[maybe_unused]] const auto* target_val, [[maybe_unused]] auto* reset_val,
843 auto&&, auto stat_type_tag, auto&&)
844 {
845 if constexpr(decltype(stat_type_tag)::value == Stat_type::S_HI_WMARK)
846 {
847 update_hi_wmark(reset_val, load(*target_val));
848 }
849 });
850} // stats_since_reset_state()
851
852template<typename Stat_set>
853void stats_mark_reset_state(Stat_set* target_stats)
854{
855 /* Reminder: Regardless of the name (it has to do with stats_since_marked_state()) we are to simply update
856 * all HI_WMARKs to their source GAUGEs' current values; and ignore all else. */
857
858 declare_stats("", nullptr, target_stats,
859 [&](auto&&, [[maybe_unused]] auto* target_val,
860 [[maybe_unused]] const auto* hi_wmark_gauged_target_val_or_null, auto stat_type_tag, auto&&)
861 {
862 if constexpr(decltype(stat_type_tag)::value == Stat_type::S_HI_WMARK)
863 {
864 store(target_val, load(*hi_wmark_gauged_target_val_or_null));
865 }
866 });
867}
868
869} // namespace flow::util::stat
Specialized class that (at a light weight) represents a printable Stat_set-member name,...
Definition: stat_set.hpp:239
Stat_name(String_view fragment={})
Constructs a Stat_name that represents only a name fragment with no reference to another Stat_name.
Definition: stat_set.cpp:30
void to_ostream(std::ostream &os) const
On-demand evaluation: os << this->str() but faster.
Definition: stat_set.cpp:70
const String_view m_fragment
Second part, if any, of the concatenation in str(), interpreted according to m_fragment_kind.
Definition: stat_set.hpp:323
std::string str() const
On-demand evaluation: Returns the full name – all fragments, from the root on, each rendered per its ...
Definition: stat_set.cpp:88
const Fragment_kind m_fragment_kind
How to render m_fragment.
Definition: stat_set.hpp:326
void append_to(std::string *target) const
On-demand evaluation: *target += this->str() but faster.
Definition: stat_set.cpp:52
const Stat_name *const m_base
First part, if any, of the concatenation in str().
Definition: stat_set.hpp:320
Fragment_kind
How a fragment of text is rendered into the name.
Definition: stat_set.hpp:245
@ S_LITERAL
The fragment is rendered verbatim.
Sub-module of flow::util providing lightweight facilities for working with simple structs of stat cou...
Definition: histo.cpp:26
void stats_mark_reset_state(Stat_set *target_stats)
Sets each HI_WMARK-declared stat-member in *target_stats to its gauged-stat's current value in *targe...
Definition: stat_set.hpp:853
@ S_HI_WMARK
Value of the same type as a specific other value stats.m_v in the same Stat_set with Stat_type S_GAUG...
@ S_GAUGE
Value (often numeric) that represents the state of something at some point in time.
@ S_ACCUMULATOR
Value (typically numeric, or conceptually a collection of numerics) that counts/tallies (typically vi...
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
void stats_aggregate_shards(Stat_set *target_stats, const It &src_stats_begin, const It &src_stats_end, const Stat_set *fresh_stats_from_0_shards)
Sharded-stats consumption: Aggregates N source Stat_sets, interpreted as shards of the aggregated who...
Definition: stat_set.hpp:577
void update_hi_wmark(T *target_val, const boost::type_identity_t< T > &val)
Default update_hi_wmark(&target_stats.m_val, val) that updates *target_val to val if and only if the ...
Definition: stat_set.hpp:388
void stats_sum(Stat_set *target_stats, const It &src_stats_begin, const It &src_stats_end)
Equivalent to stats_aggregate() as-if every stat, regardless of its actual stat::Stat_type,...
Definition: stat_set.hpp:559
void stats_aggregate_one(Stat_set *target_stats, const Stat_set &src_stats)
Aggregates the values from src_stats into *target_stats, in-place.
Definition: stat_set.hpp:508
const T & load(const T &val)
Default load(stats.m_val) that simply return stats.m_val.
Definition: stat_set.hpp:332
void stats_assign(Stat_set *target_stats, const Stat_set &src_stats)
Given a source Stat_set, makes the target Stat_set equal to the former.
Definition: stat_set.hpp:496
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 stats_reset_shard_aggregate(Stat_set *target_stats, const It &src_stats_begin, const It &src_stats_end, const Stat_set &fresh_stats_from_0_shards)
Reset-stats companion to stats_aggregate_shards(); together the two functions provide support for the...
Definition: stat_set.hpp:681
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
void stats_since_reset_state(Stat_set *target_stats, Stat_set *reset_state)
Intended for situations where the source of stat readings – including ACCUMULATORs and not just GAUGE...
Definition: stat_set.hpp:795
void stats_aggregate(Stat_set *target_stats, const It &src_stats_begin, const It &src_stats_end)
Aggregates N source Stat_sets – drawn from the iterator range [src_stats_begin, src_stats_end) – into...
Definition: stat_set.hpp:530
std::vector< std::string > stats_field_names(Ctor_args &&... ctor_args)
Returns the names of all declared stats of the given Stat_set type, in declaration order: the same na...
Definition: stat_set.hpp:442
T exchange(std::atomic< T > *target_val, boost::type_identity_t< T > val)
Equivalent to target_val->exchange(val, memory_order_relaxed); sets *target_val to val and returns th...
Definition: stat_set.hpp:382
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 stats_to_ostream(std::ostream &os, const Stat_set &stats)
Outputs a human-readable representation of all declared stats in stats to the given ostream.
Definition: stat_set.hpp:414
void stats_reset(Stat_set *target_stats, const Stat_set &fresh_stats)
With the help of a freshly-initialized Stat_set, resets the target Stat_set so as to immediately begi...
Definition: stat_set.hpp:467
Stat_set_printable< Stat_set > print(const Stat_set &stats)
Returns a lightweight proxy object that, when streamed via operator<<, pretty-prints the given Stat_s...
Definition: stat_set.hpp:436
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
Basic_string_view< char > String_view
Commonly used char-based Basic_string_view. See its doc header.
Thin proxy wrapping a const reference to a Stat_set, enabling ostream << stat::print(my_stats) syntax...
Definition: stat_set.hpp:157
const Stat_set & m_stats
The referenced stat set.
Definition: stat_set.hpp:161