userver: userver/utils/statistics/percentile.hpp Source File
Loading...
Searching...
No Matches
percentile.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/utils/statistics/percentile.hpp
4/// @brief @copybrief utils::statistics::Percentile
5
6#include <array>
7#include <atomic>
8#include <chrono>
9#include <string>
10
11#include <userver/utils/statistics/writer.hpp>
12
13USERVER_NAMESPACE_BEGIN
14
15namespace utils::statistics {
16
17/** @brief Class allows easy calculation of percentiles.
18 *
19 * Stores `M + ExtraBuckets` buckets of type `Counter`.
20 *
21 * On `Account(x)` the bucket to increment is chosen depending on `x` value.
22 * If `x` belongs to:
23 *
24 * - `[0, M)`, then bucket from `M` buckets;
25 * - `[M, M + ExtraBuckets * ExtraBucketSize)`, then bucket from `ExtraBuckets`;
26 * - all the bigger values, then last bucket from `ExtraBuckets`.
27 *
28 * On `GetPercentile(percent)` class sums up the required amount of buckets,
29 * knowing the total count of `Account(x)` invocations.
30 *
31 * @tparam M buckets count for value step of `1`
32 * @tparam Counter type of all the buckets
33 * @tparam ExtraBuckets buckets for value step of `ExtraBucketSize`
34 * @tparam ExtraBucketSize `ExtraBuckets` store values with this precision
35 * @see GetPercentile
36 * @see Account
37 *
38 * @b Example:
39 * Precisely count for first 500 milliseconds of execution using `std::uint32_t`
40 * counters and leave 100 buckets for all the other values with precision
41 * of 9 milliseconds:
42 *
43 * @code
44 * using Percentile = utils::statistics::Percentile<500, std::uint32_t, 100, 9>;
45 * using Timings = utils::statistics::RecentPeriod<Percentile, Percentile>;
46 *
47 * void Account(Percentile& perc, std::chrono::milliseconds ms) {
48 * perc.Account(ms.count());
49 * }
50 *
51 * void ExtendStatistics(utils::statistics::Writer& writer, Timings& timings) {
52 * writer["timings"]["1min"] = timings;
53 * }
54 * @endcode
55 *
56 * Type is safe to read/write concurrently from different threads/coroutines.
57 *
58 * `Percentile` metrics are non-summable, e.g. given RPS counts and timing
59 * percentiles for multiple hosts or handlers, we cannot accurately compute the
60 * total timing percentiles.
61 *
62 * @see utils::statistics::Histogram for the summable equivalent
63 */
64template <
65 std::size_t M,
66 typename Counter = std::uint32_t,
67 std::size_t ExtraBuckets = 0,
68 std::size_t ExtraBucketSize = 500>
69class Percentile final {
70public:
71 Percentile() noexcept {
72 for (auto& value : values_) value.store(0, std::memory_order_relaxed);
73 for (auto& value : extra_values_) value.store(0, std::memory_order_relaxed);
74 count_.store(0, std::memory_order_release);
75 }
76
77 Percentile(const Percentile<M, Counter, ExtraBuckets, ExtraBucketSize>& other) noexcept { *this = other; }
78
79 Percentile& operator=(const Percentile& rhs) noexcept {
80 if (this == &rhs) return *this;
81
82 std::size_t sum = 0;
83 for (std::size_t i = 0; i < values_.size(); i++) {
84 const auto value = rhs.values_[i].load(std::memory_order_relaxed);
85 values_[i].store(value, std::memory_order_relaxed);
86 sum += value;
87 }
88
89 for (std::size_t i = 0; i < extra_values_.size(); i++) {
90 const auto value = rhs.extra_values_[i].load(std::memory_order_relaxed);
91 extra_values_[i].store(value, std::memory_order_relaxed);
92 sum += value;
93 }
94
95 count_ = sum;
96 return *this;
97 }
98
99 /// @brief Account for another value.
100 ///
101 /// `1` is added to the bucket corresponding to `value`
102 void Account(std::size_t value) noexcept {
103 if (value < values_.size()) {
104 values_[value].fetch_add(1, std::memory_order_relaxed);
105 } else {
106 if (!extra_values_.empty()) {
107 std::size_t extra_bucket = (value - values_.size() + ExtraBucketSize / 2) / ExtraBucketSize;
108 if (extra_bucket >= extra_values_.size()) {
109 extra_bucket = extra_values_.size() - 1;
110 }
111 extra_values_[extra_bucket].fetch_add(1, std::memory_order_relaxed);
112 } else {
113 values_.back().fetch_add(1, std::memory_order_relaxed);
114 }
115 }
116 count_.fetch_add(1, std::memory_order_release);
117 }
118
119 /// @brief Get X percentile - min value P so that total number
120 /// of elements in buckets is no less than X percent.
121 ///
122 /// @param percent - value in [0..100] - requested percentile.
123 /// If outside of 100, then returns last bucket that has any element in it.
124 std::size_t GetPercentile(double percent) const {
125 if (count_ == 0) return 0;
126
127 std::size_t sum = 0;
128 std::size_t want_sum = count_.load(std::memory_order_acquire) * percent;
129 std::size_t max_value = 0;
130 for (std::size_t i = 0; i < values_.size(); i++) {
131 const auto value = values_[i].load(std::memory_order_relaxed);
132 sum += value;
133 if (sum * 100 > want_sum) return i;
134
135 if (value) max_value = i;
136 }
137
138 for (size_t i = 0; i < extra_values_.size(); i++) {
139 const auto value = extra_values_[i].load(std::memory_order_relaxed);
140 sum += value;
141 if (sum * 100 > want_sum) return ExtraBucketToValue(i);
142
143 if (value) max_value = ExtraBucketToValue(i);
144 }
145
146 return max_value;
147 }
148
149 template <class Duration = std::chrono::seconds>
150 void Add(
151 const Percentile<M, Counter, ExtraBuckets, ExtraBucketSize>& other,
152 [[maybe_unused]] Duration this_epoch_duration = Duration(),
153 [[maybe_unused]] Duration before_this_epoch_duration = Duration()
154 ) {
155 size_t sum = 0;
156 for (size_t i = 0; i < values_.size(); i++) {
157 const auto value = other.values_[i].load(std::memory_order_relaxed);
158 sum += value;
159 values_[i].fetch_add(value, std::memory_order_relaxed);
160 }
161
162 for (size_t i = 0; i < extra_values_.size(); i++) {
163 const auto value = other.extra_values_[i].load(std::memory_order_relaxed);
164 sum += value;
165 extra_values_[i].fetch_add(value, std::memory_order_relaxed);
166 }
167 count_.fetch_add(sum, std::memory_order_release);
168 }
169
170 /// @brief Zero out all the buckets and total number of elements.
171 void Reset() noexcept {
172 for (auto& value : values_) value.store(0, std::memory_order_relaxed);
173 for (auto& value : extra_values_) value.store(0, std::memory_order_relaxed);
174 count_ = 0;
175 }
176
177 /// @brief Total number of elements
178 Counter Count() const noexcept { return count_; }
179
180private:
181 size_t ExtraBucketToValue(std::size_t bucket) const noexcept { return values_.size() + bucket * ExtraBucketSize; }
182
183 static_assert(
184 std::atomic<Counter>::is_always_lock_free,
185 "`std::atomic<Counter>` is not lock-free. Please choose some "
186 "other `Counter` type"
187 );
188
189 std::array<std::atomic<Counter>, M> values_;
190 std::array<std::atomic<Counter>, ExtraBuckets> extra_values_;
191 std::atomic<Counter> count_;
192};
193
194std::string GetPercentileFieldName(double perc);
195
196template <size_t M, typename Counter, size_t ExtraBuckets, size_t ExtraBucketSize>
197void DumpMetric(
198 Writer& writer,
199 const Percentile<M, Counter, ExtraBuckets, ExtraBucketSize>& perc,
200 std::initializer_list<double> percents = {0, 50, 90, 95, 98, 99, 99.6, 99.9, 100}
201) {
202 for (double percent : percents) {
203 writer.ValueWithLabels(
204 perc.GetPercentile(percent), {"percentile", statistics::GetPercentileFieldName(percent)}
205 );
206 }
207}
208
209} // namespace utils::statistics
210
211USERVER_NAMESPACE_END