Network System 0.1.1
High-performance modular networking library for scalable client-server applications
Loading...
Searching...
No Matches
histogram.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2024-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
6
7#include <algorithm>
8#include <cmath>
9#include <iomanip>
10#include <limits>
11#include <sstream>
12
14
15// ============================================================================
16// histogram_snapshot implementation
17// ============================================================================
18
19auto histogram_snapshot::to_prometheus(const std::string& name) const -> std::string
20{
21 std::ostringstream oss;
22 oss << std::fixed << std::setprecision(6);
23
24 // Build label string
25 std::string label_str;
26 if (!labels.empty())
27 {
28 std::ostringstream label_oss;
29 label_oss << "{";
30 bool first = true;
31 for (const auto& [key, value] : labels)
32 {
33 if (!first)
34 {
35 label_oss << ",";
36 }
37 label_oss << key << "=\"" << value << "\"";
38 first = false;
39 }
40 label_oss << "}";
41 label_str = label_oss.str();
42 }
43
44 // Output bucket counts (cumulative)
45 for (const auto& [boundary, bucket_count] : buckets)
46 {
47 oss << name << "_bucket{le=\"";
48 if (boundary == std::numeric_limits<double>::infinity())
49 {
50 oss << "+Inf";
51 }
52 else
53 {
54 oss << boundary;
55 }
56 oss << "\"";
57 if (!labels.empty())
58 {
59 oss << "," << label_str.substr(1, label_str.length() - 2);
60 }
61 oss << "} " << bucket_count << "\n";
62 }
63
64 // Output sum and count
65 oss << name << "_sum";
66 if (!labels.empty())
67 {
68 oss << label_str;
69 }
70 oss << " " << sum << "\n";
71
72 oss << name << "_count";
73 if (!labels.empty())
74 {
75 oss << label_str;
76 }
77 oss << " " << count << "\n";
78
79 return oss.str();
80}
81
82auto histogram_snapshot::to_json() const -> std::string
83{
84 std::ostringstream oss;
85 oss << std::fixed << std::setprecision(6);
86
87 oss << "{";
88 oss << "\"count\":" << count << ",";
89 oss << "\"sum\":" << sum << ",";
90 oss << "\"min\":" << min_value << ",";
91 oss << "\"max\":" << max_value << ",";
92
93 // Percentiles
94 oss << "\"percentiles\":{";
95 bool first = true;
96 for (const auto& [p, v] : percentiles)
97 {
98 if (!first)
99 {
100 oss << ",";
101 }
102 oss << "\"" << p << "\":" << v;
103 first = false;
104 }
105 oss << "},";
106
107 // Buckets
108 oss << "\"buckets\":[";
109 first = true;
110 for (const auto& [boundary, bucket_count] : buckets)
111 {
112 if (!first)
113 {
114 oss << ",";
115 }
116 oss << "{\"le\":";
117 if (boundary == std::numeric_limits<double>::infinity())
118 {
119 oss << "\"+Inf\"";
120 }
121 else
122 {
123 oss << boundary;
124 }
125 oss << ",\"count\":" << bucket_count << "}";
126 first = false;
127 }
128 oss << "],";
129
130 // Labels
131 oss << "\"labels\":{";
132 first = true;
133 for (const auto& [key, value] : labels)
134 {
135 if (!first)
136 {
137 oss << ",";
138 }
139 oss << "\"" << key << "\":\"" << value << "\"";
140 first = false;
141 }
142 oss << "}";
143
144 oss << "}";
145
146 return oss.str();
147}
148
149// ============================================================================
150// histogram implementation
151// ============================================================================
152
154{
155 if (cfg.bucket_boundaries.empty())
156 {
158 }
159
160 boundaries_ = std::move(cfg.bucket_boundaries);
161 std::sort(boundaries_.begin(), boundaries_.end());
162
163 // Add +Inf bucket
164 boundaries_.push_back(std::numeric_limits<double>::infinity());
165
166 // Initialize bucket counts
167 bucket_count_ = boundaries_.size();
168 bucket_counts_ = std::make_unique<std::atomic<uint64_t>[]>(bucket_count_);
169 for (size_t i = 0; i < bucket_count_; ++i)
170 {
171 bucket_counts_[i].store(0, std::memory_order_relaxed);
172 }
173}
174
176 : boundaries_(std::move(other.boundaries_))
177 , bucket_counts_(std::move(other.bucket_counts_))
178 , bucket_count_(other.bucket_count_)
179 , count_(other.count_.load(std::memory_order_relaxed))
180 , sum_(other.sum_.load(std::memory_order_relaxed))
181 , min_(other.min_.load(std::memory_order_relaxed))
182 , max_(other.max_.load(std::memory_order_relaxed))
183{
184 other.bucket_count_ = 0;
185}
186
187auto histogram::operator=(histogram&& other) noexcept -> histogram&
188{
189 if (this != &other)
190 {
191 boundaries_ = std::move(other.boundaries_);
192 bucket_counts_ = std::move(other.bucket_counts_);
193 bucket_count_ = other.bucket_count_;
194 count_.store(other.count_.load(std::memory_order_relaxed), std::memory_order_relaxed);
195 sum_.store(other.sum_.load(std::memory_order_relaxed), std::memory_order_relaxed);
196 min_.store(other.min_.load(std::memory_order_relaxed), std::memory_order_relaxed);
197 max_.store(other.max_.load(std::memory_order_relaxed), std::memory_order_relaxed);
198 other.bucket_count_ = 0;
199 }
200 return *this;
201}
202
203void histogram::record(double value)
204{
205 size_t bucket_idx = find_bucket(value);
206 bucket_counts_[bucket_idx].fetch_add(1, std::memory_order_relaxed);
207
208 count_.fetch_add(1, std::memory_order_relaxed);
209 add_to_sum(value);
210 update_min(value);
211 update_max(value);
212}
213
214auto histogram::count() const -> uint64_t
215{
216 return count_.load(std::memory_order_relaxed);
217}
218
219auto histogram::sum() const -> double
220{
221 return sum_.load(std::memory_order_relaxed);
222}
223
224auto histogram::min() const -> double
225{
226 return min_.load(std::memory_order_relaxed);
227}
228
229auto histogram::max() const -> double
230{
231 return max_.load(std::memory_order_relaxed);
232}
233
234auto histogram::mean() const -> double
235{
236 uint64_t c = count();
237 if (c == 0)
238 {
239 return 0.0;
240 }
241 return sum() / static_cast<double>(c);
242}
243
244auto histogram::percentile(double p) const -> double
245{
246 std::lock_guard<std::mutex> lock(mutex_);
247
248 uint64_t total = count_.load(std::memory_order_relaxed);
249 if (total == 0)
250 {
251 return 0.0;
252 }
253
254 // Clamp percentile to valid range
255 p = std::clamp(p, 0.0, 1.0);
256
257 // Target count for this percentile
258 double target = p * static_cast<double>(total);
259
260 // Calculate cumulative counts
261 std::vector<uint64_t> cumulative;
262 cumulative.reserve(bucket_count_);
263 uint64_t running_sum = 0;
264 for (size_t i = 0; i < bucket_count_; ++i)
265 {
266 running_sum += bucket_counts_[i].load(std::memory_order_relaxed);
267 cumulative.push_back(running_sum);
268 }
269
270 // Find the bucket containing the target count
271 size_t bucket_idx = 0;
272 for (size_t i = 0; i < cumulative.size(); ++i)
273 {
274 if (static_cast<double>(cumulative[i]) >= target)
275 {
276 bucket_idx = i;
277 break;
278 }
279 }
280
281 // Linear interpolation within the bucket
282 double lower_bound = (bucket_idx == 0) ? 0.0 : boundaries_[bucket_idx - 1];
283 double upper_bound = boundaries_[bucket_idx];
284
285 // Handle infinity bucket
286 if (std::isinf(upper_bound))
287 {
288 return lower_bound;
289 }
290
291 uint64_t lower_count = (bucket_idx == 0) ? 0 : cumulative[bucket_idx - 1];
292 uint64_t upper_count = cumulative[bucket_idx];
293
294 if (upper_count == lower_count)
295 {
296 return lower_bound;
297 }
298
299 double fraction = (target - static_cast<double>(lower_count))
300 / (static_cast<double>(upper_count) - static_cast<double>(lower_count));
301
302 return lower_bound + (fraction * (upper_bound - lower_bound));
303}
304
305auto histogram::buckets() const -> std::vector<std::pair<double, uint64_t>>
306{
307 std::lock_guard<std::mutex> lock(mutex_);
308
309 std::vector<std::pair<double, uint64_t>> result;
310 result.reserve(bucket_count_);
311
312 uint64_t cumulative = 0;
313 for (size_t i = 0; i < bucket_count_; ++i)
314 {
315 cumulative += bucket_counts_[i].load(std::memory_order_relaxed);
316 result.emplace_back(boundaries_[i], cumulative);
317 }
318
319 return result;
320}
321
322auto histogram::snapshot(const std::map<std::string, std::string>& labels) const
324{
326 snap.count = count();
327 snap.sum = sum();
328 snap.min_value = min();
329 snap.max_value = max();
330 snap.labels = labels;
331
332 // Calculate percentiles
333 snap.percentiles[0.5] = p50();
334 snap.percentiles[0.9] = percentile(0.9);
335 snap.percentiles[0.95] = p95();
336 snap.percentiles[0.99] = p99();
337 snap.percentiles[0.999] = p999();
338
339 // Get buckets
340 snap.buckets = buckets();
341
342 return snap;
343}
344
346{
347 std::lock_guard<std::mutex> lock(mutex_);
348
349 count_.store(0, std::memory_order_relaxed);
350 sum_.store(0.0, std::memory_order_relaxed);
351 min_.store(std::numeric_limits<double>::infinity(), std::memory_order_relaxed);
352 max_.store(-std::numeric_limits<double>::infinity(), std::memory_order_relaxed);
353
354 for (size_t i = 0; i < bucket_count_; ++i)
355 {
356 bucket_counts_[i].store(0, std::memory_order_relaxed);
357 }
358}
359
360auto histogram::find_bucket(double value) const -> size_t
361{
362 auto it = std::lower_bound(boundaries_.begin(), boundaries_.end(), value);
363 if (it == boundaries_.end())
364 {
365 return boundaries_.size() - 1;
366 }
367 return static_cast<size_t>(std::distance(boundaries_.begin(), it));
368}
369
370void histogram::update_min(double value)
371{
372 double current = min_.load(std::memory_order_relaxed);
373 while (value < current && !min_.compare_exchange_weak(current, value, std::memory_order_relaxed,
374 std::memory_order_relaxed))
375 {
376 // current is updated by compare_exchange_weak
377 }
378}
379
380void histogram::update_max(double value)
381{
382 double current = max_.load(std::memory_order_relaxed);
383 while (value > current && !max_.compare_exchange_weak(current, value, std::memory_order_relaxed,
384 std::memory_order_relaxed))
385 {
386 // current is updated by compare_exchange_weak
387 }
388}
389
390void histogram::add_to_sum(double value)
391{
392 double current = sum_.load(std::memory_order_relaxed);
393 while (!sum_.compare_exchange_weak(current, current + value, std::memory_order_relaxed,
394 std::memory_order_relaxed))
395 {
396 // current is updated by compare_exchange_weak
397 }
398}
399
400} // namespace kcenon::network::metrics
Thread-safe histogram for capturing value distributions.
Definition histogram.h:106
auto max() const -> double
Get maximum observed value.
auto find_bucket(double value) const -> size_t
Find bucket index for a value.
void record(double value)
Record a value observation.
auto min() const -> double
Get minimum observed value.
auto operator=(const histogram &) -> histogram &=delete
auto snapshot(const std::map< std::string, std::string > &labels={}) const -> histogram_snapshot
Create immutable snapshot of current state.
void update_max(double value)
Update max value atomically.
std::vector< double > boundaries_
Definition histogram.h:221
std::unique_ptr< std::atomic< uint64_t >[]> bucket_counts_
Definition histogram.h:222
auto sum() const -> double
Get sum of all observations.
std::atomic< uint64_t > count_
Definition histogram.h:224
auto mean() const -> double
Get mean of all observations.
histogram(histogram_config cfg=histogram_config::default_latency_config())
Construct histogram with configuration.
auto percentile(double p) const -> double
Calculate percentile value.
void add_to_sum(double value)
Add to sum atomically.
auto count() const -> uint64_t
Get total number of observations.
auto buckets() const -> std::vector< std::pair< double, uint64_t > >
Get all bucket counts.
void reset()
Reset all statistics.
void update_min(double value)
Update min value atomically.
Histogram metric implementation for latency distribution tracking.
Configuration for histogram bucket boundaries.
Definition histogram.h:34
std::vector< double > bucket_boundaries
Explicit bucket boundaries (upper bounds)
Definition histogram.h:41
static auto default_latency_config() -> histogram_config
Create default configuration for network latencies.
Definition histogram.h:47
Immutable snapshot of histogram state for export.
Definition histogram.h:60
std::map< double, double > percentiles
Percentile -> value mapping.
Definition histogram.h:65
std::map< std::string, std::string > labels
Additional metric labels.
Definition histogram.h:67
uint64_t count
Total number of observations.
Definition histogram.h:61
auto to_json() const -> std::string
Export histogram as JSON.
Definition histogram.cpp:82
double sum
Sum of all observed values.
Definition histogram.h:62
double min_value
Minimum observed value.
Definition histogram.h:63
std::vector< std::pair< double, uint64_t > > buckets
Boundary -> cumulative count.
Definition histogram.h:66
auto to_prometheus(const std::string &name) const -> std::string
Export histogram in Prometheus format.
Definition histogram.cpp:19
double max_value
Maximum observed value.
Definition histogram.h:64