diff --git a/core/include/userver/utils/statistics/histogram_view.hpp b/core/include/userver/utils/statistics/histogram_view.hpp index 754607a10522..10cbdc7ddfaa 100644 --- a/core/include/userver/utils/statistics/histogram_view.hpp +++ b/core/include/userver/utils/statistics/histogram_view.hpp @@ -45,6 +45,16 @@ class HistogramView final { /// Returns the sum of counts from all buckets. std::uint64_t GetTotalCount() const noexcept; + /// Returns sum of values from the given bucket. + double GetSumAt(std::size_t index) const; + + // Returns sum of values from the "infinity" bucket + /// (greater than the largest bucket boundary). + double GetSumAtInf() const noexcept; + + /// Returns sum of values from all buckets. + double GetTotalSum() const noexcept; + private: friend struct impl::histogram::Access; diff --git a/core/include/userver/utils/statistics/impl/histogram_bucket.hpp b/core/include/userver/utils/statistics/impl/histogram_bucket.hpp index 99aefeb1ab1c..e9786e65ce5a 100644 --- a/core/include/userver/utils/statistics/impl/histogram_bucket.hpp +++ b/core/include/userver/utils/statistics/impl/histogram_bucket.hpp @@ -31,6 +31,7 @@ struct Bucket final { BoundOrSize upper_bound{0.0}; std::atomic counter{0}; + std::atomic sum{0.0}; }; void CopyBounds(Bucket* bucket_array, utils::span upper_bounds); diff --git a/core/src/utils/statistics/histogram.cpp b/core/src/utils/statistics/histogram.cpp index 501d726a3053..6144034f8152 100644 --- a/core/src/utils/statistics/histogram.cpp +++ b/core/src/utils/statistics/histogram.cpp @@ -154,6 +154,7 @@ void Histogram::Account(double value, std::uint64_t count) noexcept { pre_bucket_index + 1 > bucket_count_ ? 0 : pre_bucket_index + 1; auto& bucket = buckets_[bucket_index]; bucket.counter.fetch_add(count, std::memory_order_relaxed); + impl::histogram::AddAtomic(bucket.sum, value * count); } void ResetMetric(Histogram& histogram) noexcept { diff --git a/core/src/utils/statistics/histogram_test.cpp b/core/src/utils/statistics/histogram_test.cpp index 42863964389e..a1b122080452 100644 --- a/core/src/utils/statistics/histogram_test.cpp +++ b/core/src/utils/statistics/histogram_test.cpp @@ -145,6 +145,18 @@ UTEST(StatisticsHistogram, Reset) { EXPECT_EQ(histogram.GetView(), zero_histogram.GetView()); } +UTEST(StatisticsHistogram, Sum) { + utils::statistics::Histogram histogram{Bounds()}; + AccountSome(histogram); + + EXPECT_EQ(histogram.GetView().GetSumAt(0), 1.2); + EXPECT_EQ(histogram.GetView().GetSumAt(1), 1.8); + EXPECT_EQ(histogram.GetView().GetSumAt(2), 10 + 30 * 4); + EXPECT_EQ(histogram.GetView().GetSumAt(3), 0.0); + EXPECT_EQ(histogram.GetView().GetSumAtInf(), 100); + EXPECT_EQ(histogram.GetView().GetTotalSum(), 1.2 + 1.8 + 10 + 30 * 4 + 100); +} + UTEST(StatisticsHistogram, ZeroBuckets) { utils::statistics::Histogram histogram{std::vector{}}; EXPECT_EQ(histogram.GetView().GetBucketCount(), 0); @@ -283,7 +295,8 @@ UTEST_F(StatisticsHistogramFormat, JsonFormat) { "value": { "bounds": [1.5, 5.0, 42.0, 60.0], "buckets": [1, 1, 5, 0], - "inf": 1 + "inf": 1, + "sum":233.0 }, "labels": {}, "type": "HIST_RATE" @@ -306,6 +319,7 @@ test_bucket{le="42"} 7 test_bucket{le="60"} 7 test_bucket{le="+Inf"} 8 test_count{} 8 +test_sum{} 233 )"; EXPECT_EQ(utils::statistics::ToPrometheusFormat(GetStorage()), expected); } @@ -318,6 +332,7 @@ test_bucket{le="42"} 7 test_bucket{le="60"} 7 test_bucket{le="+Inf"} 8 test_count{} 8 +test_sum{} 233 )"; EXPECT_EQ(utils::statistics::ToPrometheusFormatUntyped(GetStorage()), expected); @@ -340,6 +355,7 @@ test_bucket{le="42",custom_label_1="1",custom_label_2="2"} 7 test_bucket{le="60",custom_label_1="1",custom_label_2="2"} 7 test_bucket{le="+Inf",custom_label_1="1",custom_label_2="2"} 8 test_count{custom_label_1="1",custom_label_2="2"} 8 +test_sum{custom_label_1="1",custom_label_2="2"} 233 )"; EXPECT_EQ(utils::statistics::ToPrometheusFormat(storage), expected); } @@ -364,7 +380,8 @@ UTEST_F(StatisticsHistogramFormat, SolomonFormat) { "hist": { "bounds": [1.5, 5.0, 42.0, 60.0], "buckets": [1, 1, 5, 0], - "inf": 1 + "inf": 1, + "sum": 233.0 }, "type": "HIST_RATE" } diff --git a/core/src/utils/statistics/histogram_view.cpp b/core/src/utils/statistics/histogram_view.cpp index 0355e71f9dcf..6cb3d4e62308 100644 --- a/core/src/utils/statistics/histogram_view.cpp +++ b/core/src/utils/statistics/histogram_view.cpp @@ -47,6 +47,25 @@ std::uint64_t HistogramView::GetTotalCount() const noexcept { return total; } +double HistogramView::GetSumAt(std::size_t index) const { + UASSERT(index < GetBucketCount()); + return buckets_[index + 1].sum.load(std::memory_order_relaxed); +} + +double HistogramView::GetSumAtInf() const noexcept { + UASSERT(buckets_); + return buckets_[0].sum.load(std::memory_order_relaxed); +} + +double HistogramView::GetTotalSum() const noexcept { + const auto bucket_count = GetBucketCount(); + auto total = GetSumAtInf(); + for (std::size_t i = 0; i < bucket_count; ++i) { + total += GetSumAt(i); + } + return total; +} + void DumpMetric(Writer& writer, HistogramView histogram) { writer = histogram; } bool operator==(HistogramView lhs, HistogramView rhs) noexcept { diff --git a/core/src/utils/statistics/impl/histogram_bucket.cpp b/core/src/utils/statistics/impl/histogram_bucket.cpp index 479e788ae841..e0659bbb7ced 100644 --- a/core/src/utils/statistics/impl/histogram_bucket.cpp +++ b/core/src/utils/statistics/impl/histogram_bucket.cpp @@ -8,13 +8,16 @@ namespace utils::statistics::impl::histogram { Bucket::Bucket(const Bucket& other) noexcept : upper_bound(other.upper_bound), - counter(other.counter.load(std::memory_order_relaxed)) {} + counter(other.counter.load(std::memory_order_relaxed)), + sum(other.sum.load(std::memory_order_relaxed)) {} Bucket& Bucket::operator=(const Bucket& other) noexcept { if (this == &other) return *this; upper_bound = other.upper_bound; counter.store(other.counter.load(std::memory_order_relaxed), std::memory_order_relaxed); + sum.store(other.sum.load(std::memory_order_relaxed), + std::memory_order_relaxed); return *this; } diff --git a/core/src/utils/statistics/impl/histogram_serialization.hpp b/core/src/utils/statistics/impl/histogram_serialization.hpp index dd9557174d9f..2ee66f9be1e6 100644 --- a/core/src/utils/statistics/impl/histogram_serialization.hpp +++ b/core/src/utils/statistics/impl/histogram_serialization.hpp @@ -14,6 +14,7 @@ Value Serialize(HistogramView value, formats::serialize::To) { result["bounds"] = impl::histogram::Access::Bounds(value); result["buckets"] = impl::histogram::Access::Values(value); result["inf"] = value.GetValueAtInf(); + result["sum"] = value.GetTotalSum(); return result.ExtractValue(); } @@ -26,6 +27,8 @@ void WriteToStream(HistogramView value, StringBuilder& sw) { WriteToStream(impl::histogram::Access::Values(value), sw); sw.Key("inf"); WriteToStream(value.GetValueAtInf(), sw); + sw.Key("sum"); + WriteToStream(value.GetTotalSum(), sw); } } // namespace utils::statistics diff --git a/core/src/utils/statistics/impl/histogram_view_utils.hpp b/core/src/utils/statistics/impl/histogram_view_utils.hpp index 4d41e7b80ed3..779e2f40b2a9 100644 --- a/core/src/utils/statistics/impl/histogram_view_utils.hpp +++ b/core/src/utils/statistics/impl/histogram_view_utils.hpp @@ -51,6 +51,23 @@ inline void AddNonAtomic(std::atomic& to, std::uint64_t x) { to.store(to.load(std::memory_order_relaxed) + x, std::memory_order_relaxed); } +inline void AddNonAtomic(std::atomic& to, std::uint64_t x) { + to.store(to.load(std::memory_order_relaxed) + x, std::memory_order_relaxed); +} + +inline void AddAtomic(std::atomic& to, double x) { +#if __cplusplus >= 202002L + to += x; +#else + double expected = to.load(); + double desired = expected + x; + while (!to.compare_exchange_weak(expected, desired, std::memory_order_release, + std::memory_order_relaxed)) { + desired = expected + x; + } +#endif +} + inline bool IsBoundPositive(double x) noexcept { return std::isnormal(x) && x > 0; } @@ -105,6 +122,7 @@ class MutableView final { void Assign(HistogramView other) const noexcept { buckets_[0].upper_bound.size = other.GetBucketCount(); buckets_[0].counter.store(other.GetValueAtInf(), std::memory_order_relaxed); + buckets_[0].sum.store(other.GetSumAtInf(), std::memory_order_relaxed); boost::copy(Access::Buckets(other), Access::Buckets(*this).begin()); } @@ -115,13 +133,16 @@ class MutableView final { boost::upper_bound(bounds_view, value, std::less_equal<>{}); auto& bucket = (iter == bounds_view.end()) ? buckets_[0] : *iter.base(); bucket.counter.fetch_add(count, std::memory_order_relaxed); + AddAtomic(bucket.sum, value * count); } // Atomic void Reset() const noexcept { buckets_[0].counter.store(0, std::memory_order_relaxed); + buckets_[0].sum.store(0.0, std::memory_order_relaxed); for (auto& bucket : Access::Buckets(*this)) { bucket.counter.store(0, std::memory_order_relaxed); + bucket.sum.store(0.0, std::memory_order_relaxed); } } @@ -131,6 +152,8 @@ class MutableView final { boost::range::includes(Access::Bounds(other), Access::Bounds(*this)), "Buckets can be merged, but not added during Histogram conversion."); AddNonAtomic(buckets_[0].counter, other.GetValueAtInf()); + AddNonAtomic(buckets_[0].sum, other.GetSumAtInf()); + const auto self_bounds = Access::Bounds(*this); auto current_self_bound = self_bounds.begin(); for (const auto& other_bucket : Access::Buckets(other)) { @@ -142,6 +165,7 @@ class MutableView final { ? buckets_[0] : *current_self_bound.base(); AddNonAtomic(self_bucket.counter, other_bucket.counter); + AddNonAtomic(self_bucket.sum, other_bucket.sum); } } diff --git a/core/src/utils/statistics/prometheus.cpp b/core/src/utils/statistics/prometheus.cpp index 37d557aca9a1..c04326d3873f 100644 --- a/core/src/utils/statistics/prometheus.cpp +++ b/core/src/utils/statistics/prometheus.cpp @@ -85,6 +85,9 @@ class FormatBuilder final : public utils::statistics::BaseFormatBuilder { AppendHistogramMetric("count", prometheus_name, /* upper_bound */ "", fmt::to_string(histogram.GetTotalCount()), labels); + AppendHistogramMetric("sum", prometheus_name, + /* upper_bound */ "", + fmt::to_string(histogram.GetTotalSum()), labels); } void DumpMetricNameAndType(std::string_view name, const MetricValue& value) {