Entity Generator 4.2.0
Composable, deterministic entity generation for C++23
Loading...
Searching...
No Matches
stats_observer.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2020-2026 Diego Dasso Migotto
3
4#ifndef DASMIG_EXT_STATS_OBSERVER_HPP
5#define DASMIG_EXT_STATS_OBSERVER_HPP
6
7#include "../entitygen.hpp"
8#include <algorithm>
9#include <chrono>
10#include <iomanip>
11#include <limits>
12#include <map>
13#include <sstream>
14#include <string>
15
16namespace dasmig::ext
17{
18
19/// @file stats_observer.hpp
20/// @brief Observer that collects comprehensive generation statistics.
21
22/// @brief Observer that collects comprehensive generation statistics.
23///
24/// All fields are public for direct access. Use reset() to zero everything
25/// and report() to produce a formatted summary.
26/// @see generation_observer, eg::add_observer()
28{
29 public:
30 using clock = std::chrono::steady_clock; ///< Clock type used for timing.
31 using duration = std::chrono::steady_clock::duration; ///< Duration type.
32
33 /// @name Entity Counters
34 /// @{
35 std::size_t entities_generated{0}; ///< Total entities successfully generated.
36 std::size_t entity_retries{0}; ///< Total entity-level validation retries.
37 std::size_t entity_failures{0}; ///< Total entity-level validation failures.
38 /// @}
39
40 /// @name Entity Timing
41 /// @{
42 duration total_generation_time{duration::zero()}; ///< Wall-clock time summed across all entities.
43 duration min_entity_time{duration::max()}; ///< Fastest entity generation.
44 duration max_entity_time{duration::zero()}; ///< Slowest entity generation.
45 /// @}
46
47 /// @name Component Counters
48 /// @{
49 std::size_t components_generated{0}; ///< Total components successfully generated.
50 std::size_t components_skipped{0}; ///< Total components skipped (weight or conditional).
51 std::size_t component_failures{0}; ///< Total component validation failures.
52 /// @}
53
54 /// @name Per-Component-Key Counters
55 /// @{
56 std::map<std::wstring, std::size_t> component_retries; ///< Total retries per key.
57 std::map<std::wstring, std::size_t> component_counts; ///< Successful generations per key.
58 std::map<std::wstring, std::size_t> component_skip_counts; ///< Skips per key.
59 std::map<std::wstring, std::size_t> component_failure_counts;///< Failures per key.
60 /// @}
61
62 /// @name Per-Component-Key Timing
63 /// @{
64 std::map<std::wstring, duration> component_times; ///< Total generation time per key.
65 std::map<std::wstring, duration> component_min_times; ///< Fastest per key.
66 std::map<std::wstring, duration> component_max_times; ///< Slowest per key.
67 /// @}
68
69 /// @name Components-per-Entity Tracking
70 /// @{
72 std::numeric_limits<std::size_t>::max()}; ///< Fewest components in any entity.
73 std::size_t max_components_per_entity{0}; ///< Most components in any entity.
74 std::size_t total_components_in_entities{0}; ///< Sum for computing the average.
75 /// @}
76
77 /// @name Value Distribution
78 /// @{
79
80 /// @brief Per-component key, counts how many times each display string appeared.
81 ///
82 /// Useful for analysing uniformity of random components.
83 std::map<std::wstring,
84 std::map<std::wstring, std::size_t>> value_distribution;
85 /// @}
86
87 /// @name Computed Helpers
88 /// @{
89
90 /// @brief Average generation time per entity (zero if none generated).
91 [[nodiscard]] duration avg_entity_time() const
92 {
93 if (entities_generated == 0) return duration::zero();
95 }
96
97 /// @brief Average generation time per component key.
98 /// @param key The component key.
99 /// @return Average duration, or zero if no data for the key.
100 [[nodiscard]] duration avg_component_time(const std::wstring& key) const
101 {
102 auto it = component_times.find(key);
103 auto ct = component_counts.find(key);
104 if (it == component_times.end() || ct == component_counts.end()
105 || ct->second == 0)
106 return duration::zero();
107 return it->second / ct->second;
108 }
109
110 /// @brief Average components produced per entity (0.0 if none generated).
111 [[nodiscard]] double avg_components_per_entity() const
112 {
113 if (entities_generated == 0) return 0.0;
114 return static_cast<double>(total_components_in_entities)
115 / static_cast<double>(entities_generated);
116 }
117
118 /// @brief Retry rate: total retries / total component generations.
119 [[nodiscard]] double component_retry_rate() const
120 {
121 if (components_generated == 0) return 0.0;
122 std::size_t total{0};
123 for (const auto& [_, n] : component_retries) total += n;
124 return static_cast<double>(total)
125 / static_cast<double>(components_generated);
126 }
127
128 /// @brief Entity retry rate: entity retries / entities generated.
129 [[nodiscard]] double entity_retry_rate() const
130 {
131 if (entities_generated == 0) return 0.0;
132 return static_cast<double>(entity_retries)
133 / static_cast<double>(entities_generated);
134 }
135
136 /// @}
137 /// @name Report
138 /// @{
139
140 /// @brief Produce a human-readable wide-string report of all collected stats.
141 /// @return A multi-line formatted report string.
142 // One straight-line report; splitting it would only scatter the format.
143 [[nodiscard]] std::wstring report() const // NOLINT(readability-function-cognitive-complexity)
144 {
145 std::wostringstream o;
146 o << std::fixed << std::setprecision(2);
147
148 auto us = [](duration d) {
149 return std::chrono::duration_cast<
150 std::chrono::microseconds>(d).count();
151 };
152
153 // -- Entity summary ------------------------------------------------
154 o << L"=== Entity Summary ===\n"
155 << L" Generated : " << entities_generated << L'\n'
156 << L" Retries : " << entity_retries << L'\n'
157 << L" Failures : " << entity_failures << L'\n';
158
159 if (entities_generated > 0)
160 {
161 o << L" Retry rate: " << (entity_retry_rate() * 100.0) << L" %\n";
162 }
163
164 // -- Entity timing -------------------------------------------------
165 o << L"\n=== Entity Timing ===\n"
166 << L" Total : " << us(total_generation_time) << L" us\n";
167
168 if (entities_generated > 0)
169 {
170 o << L" Avg : " << us(avg_entity_time()) << L" us\n"
171 << L" Min : " << us(min_entity_time) << L" us\n"
172 << L" Max : " << us(max_entity_time) << L" us\n";
173 }
174
175 // -- Components per entity -----------------------------------------
176 o << L"\n=== Components per Entity ===\n";
177
178 if (entities_generated > 0)
179 {
180 o << L" Avg : " << avg_components_per_entity() << L'\n'
181 << L" Min : " << min_components_per_entity << L'\n'
182 << L" Max : " << max_components_per_entity << L'\n';
183 }
184 else
185 {
186 o << L" (no entities generated)\n";
187 }
188
189 // -- Component summary ---------------------------------------------
190 o << L"\n=== Component Summary ===\n"
191 << L" Generated : " << components_generated << L'\n'
192 << L" Skipped : " << components_skipped << L'\n'
193 << L" Failures : " << component_failures << L'\n';
194
195 if (components_generated > 0)
196 {
197 o << L" Retry rate: " << (component_retry_rate() * 100.0)
198 << L" %\n";
199 }
200
201 // -- Per-key breakdown ---------------------------------------------
202 // Collect all keys that appear in any per-key map.
203 std::map<std::wstring, int> all_keys;
204 for (const auto& [k, _] : component_counts) all_keys[k];
205 for (const auto& [k, _] : component_skip_counts) all_keys[k];
206 for (const auto& [k, _] : component_failure_counts) all_keys[k];
207 for (const auto& [k, _] : component_retries) all_keys[k];
208
209 if (!all_keys.empty())
210 {
211 o << L"\n=== Per-Component Breakdown ===\n";
212
213 for (const auto& [key, _] : all_keys)
214 {
215 o << L"\n [" << key << L"]\n";
216
217 auto count_or = [](const auto& m, const std::wstring& k)
218 -> std::size_t {
219 auto it = m.find(k);
220 return it != m.end() ? it->second : 0;
221 };
222
223 auto gen_n = count_or(component_counts, key);
224 auto skip_n = count_or(component_skip_counts, key);
225 auto fail_n = count_or(component_failure_counts, key);
226 auto ret_n = count_or(component_retries, key);
227
228 o << L" Generated : " << gen_n << L'\n'
229 << L" Skipped : " << skip_n << L'\n'
230 << L" Retries : " << ret_n << L'\n'
231 << L" Failures : " << fail_n << L'\n';
232
233 if (auto it = component_times.find(key);
234 it != component_times.end())
235 {
236 o << L" Time total: " << us(it->second) << L" us\n"
237 << L" Time avg : "
238 << us(avg_component_time(key)) << L" us\n";
239 }
240
241 if (auto it = component_min_times.find(key);
242 it != component_min_times.end())
243 {
244 o << L" Time min : " << us(it->second) << L" us\n";
245 }
246
247 if (auto it = component_max_times.find(key);
248 it != component_max_times.end())
249 {
250 o << L" Time max : " << us(it->second) << L" us\n";
251 }
252 }
253 }
254
255 // -- Value distribution --------------------------------------------
256 if (!value_distribution.empty())
257 {
258 o << L"\n=== Value Distribution ===\n";
259
260 for (const auto& [key, dist] : value_distribution)
261 {
262 o << L"\n [" << key << L"] ("
263 << dist.size() << L" distinct values)\n";
264
265 // Sort by count descending for readability.
266 std::vector<std::pair<std::wstring, std::size_t>> sorted(
267 dist.begin(), dist.end());
268 std::ranges::sort(sorted, [](const auto& a, const auto& b) {
269 return a.second > b.second;
270 });
271
272 auto total_n = count_for(key);
273 for (const auto& [val, n] : sorted)
274 {
275 const double pct = total_n > 0
276 ? static_cast<double>(n)
277 / static_cast<double>(total_n) * 100.0
278 : 0.0;
279 o << L" " << val << L" : " << n
280 << L" (" << pct << L" %)\n";
281 }
282 }
283 }
284
285 return o.str();
286 }
287
288 /// @}
289 /// @name Reset
290 /// @{
291
292 /// @brief Zero all counters, timers, and distributions.
293 void reset()
294 {
296 entity_retries = 0;
297 entity_failures = 0;
298
299 total_generation_time = duration::zero();
300 min_entity_time = duration::max();
301 max_entity_time = duration::zero();
302
306
307 component_retries.clear();
308 component_counts.clear();
309 component_skip_counts.clear();
311
312 component_times.clear();
313 component_min_times.clear();
314 component_max_times.clear();
315
316 min_components_per_entity = std::numeric_limits<std::size_t>::max();
319
320 value_distribution.clear();
321 }
322
323 /// @}
324 /// @name Hook Overrides
325 /// @{
326
327 void on_before_generate() override
328 {
329 _entity_start = clock::now();
330 _current_entity_components = 0;
331 }
332
333 void on_after_generate(const entity& /*e*/) override
334 {
336
337 auto elapsed = clock::now() - _entity_start;
338 total_generation_time += elapsed;
339 min_entity_time = std::min(min_entity_time, elapsed);
340 max_entity_time = std::max(max_entity_time, elapsed);
341
342 total_components_in_entities += _current_entity_components;
343 min_components_per_entity = std::min(min_components_per_entity, _current_entity_components);
344 max_components_per_entity = std::max(max_components_per_entity, _current_entity_components);
345 }
346
347 void on_before_component(const std::wstring& key) override
348 {
349 _component_start[key] = clock::now();
350 }
351
352 void on_after_component(const std::wstring& key,
353 const std::any& value) override
354 {
356 ++component_counts[key];
357 ++_current_entity_components;
358
359 auto it = _component_start.find(key);
360 if (it != _component_start.end())
361 {
362 auto elapsed = clock::now() - it->second;
363 component_times[key] += elapsed;
364
365 auto& mn = component_min_times[key];
366 auto& mx = component_max_times[key];
367 if (component_counts[key] == 1)
368 {
369 mn = elapsed;
370 mx = elapsed;
371 }
372 else
373 {
374 if (elapsed < mn) mn = elapsed;
375 if (elapsed > mx) mx = elapsed;
376 }
377 }
378
379 // Record value distribution via the component's display string.
380 // on_after_component only receives (key, value). The entity hasn't
381 // stored the display string yet, so we use any_cast on common
382 // types to produce a representative key. A catch-all records
383 // "<other>" for types we can't stringify cheaply.
384 value_distribution[key][display_for(value)]++;
385 }
386
387 void on_skip(const std::wstring& key) override
388 {
391 }
392
393 void on_after_retry(const std::wstring& key,
394 std::size_t /*attempt*/,
395 const std::any& /*value*/) override
396 {
397 ++component_retries[key];
398 }
399
400 void on_component_fail(const std::wstring& key) override
401 {
404 }
405
406 void on_after_entity_retry(std::size_t /*attempt*/) override
407 {
409 }
410
411 void on_entity_fail() override
412 {
414 }
415
416 private:
417 // Timing state (not part of the public stats).
418 clock::time_point _entity_start;
419 std::map<std::wstring, clock::time_point> _component_start;
420 std::size_t _current_entity_components{0};
421
422 // Best-effort display string for value distribution tracking.
423 static std::wstring display_for(const std::any& value)
424 {
425 if (const auto* ws = std::any_cast<std::wstring>(&value))
426 return *ws;
427 if (const auto* i = std::any_cast<int>(&value))
428 return std::to_wstring(*i);
429 if (const auto* d = std::any_cast<double>(&value))
430 return std::to_wstring(*d);
431 if (const auto* f = std::any_cast<float>(&value))
432 return std::to_wstring(*f);
433 if (const auto* l = std::any_cast<long>(&value))
434 return std::to_wstring(*l);
435 if (const auto* b = std::any_cast<bool>(&value))
436 return *b ? L"true" : L"false";
437 return L"<other>";
438 }
439
440 // Total generation count for a key (used by report).
441 [[nodiscard]] std::size_t count_for(const std::wstring& key) const
442 {
443 auto it = component_counts.find(key);
444 return it != component_counts.end() ? it->second : 0;
445 }
446};
447
448/// @brief Stream operator for one-liner report output.
449inline std::wostream& operator<<(std::wostream& os,
450 const stats_observer& stats)
451{
452 return os << stats.report();
453}
454
455} // namespace dasmig::ext
456
457#endif // DASMIG_EXT_STATS_OBSERVER_HPP
A generated entity holding component values in registration order.
Observer that collects comprehensive generation statistics.
std::map< std::wstring, std::size_t > component_retries
Total retries per key.
std::map< std::wstring, std::size_t > component_counts
Successful generations per key.
void on_skip(const std::wstring &key) override
Called when a component is skipped (weight roll or conditional exclusion).
double component_retry_rate() const
Retry rate: total retries / total component generations.
std::map< std::wstring, std::size_t > component_skip_counts
Skips per key.
std::size_t entities_generated
Total entities successfully generated.
std::chrono::steady_clock clock
Clock type used for timing.
std::chrono::steady_clock::duration duration
Duration type.
duration min_entity_time
Fastest entity generation.
void on_after_entity_retry(std::size_t) override
Called after an entity validation retry.
std::map< std::wstring, std::map< std::wstring, std::size_t > > value_distribution
Per-component key, counts how many times each display string appeared.
std::map< std::wstring, std::size_t > component_failure_counts
Failures per key.
std::size_t entity_retries
Total entity-level validation retries.
void reset()
Zero all counters, timers, and distributions.
std::map< std::wstring, duration > component_max_times
Slowest per key.
duration avg_component_time(const std::wstring &key) const
Average generation time per component key.
void on_before_component(const std::wstring &key) override
Called before a component is generated.
void on_entity_fail() override
Called when entity validation is exhausted (precedes exception).
std::size_t total_components_in_entities
Sum for computing the average.
double entity_retry_rate() const
Entity retry rate: entity retries / entities generated.
std::size_t min_components_per_entity
Fewest components in any entity.
void on_after_retry(const std::wstring &key, std::size_t, const std::any &) override
Called after a component validation retry.
std::size_t max_components_per_entity
Most components in any entity.
std::wstring report() const
Produce a human-readable wide-string report of all collected stats.
duration max_entity_time
Slowest entity generation.
duration total_generation_time
Wall-clock time summed across all entities.
std::size_t entity_failures
Total entity-level validation failures.
std::size_t components_skipped
Total components skipped (weight or conditional).
void on_component_fail(const std::wstring &key) override
Called when component validation is exhausted (precedes exception).
void on_after_generate(const entity &) override
Called after an entity is successfully generated.
double avg_components_per_entity() const
Average components produced per entity (0.0 if none generated).
duration avg_entity_time() const
Average generation time per entity (zero if none generated).
std::size_t components_generated
Total components successfully generated.
std::map< std::wstring, duration > component_times
Total generation time per key.
void on_before_generate() override
Called before an entity is generated.
std::size_t component_failures
Total component validation failures.
void on_after_component(const std::wstring &key, const std::any &value) override
Called after a component is successfully generated.
std::map< std::wstring, duration > component_min_times
Fastest per key.
Observer interface for hooking into generation lifecycle events.
std::wostream & operator<<(std::wostream &os, const stats_observer &stats)
Stream operator for one-liner report output.