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