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_) {
73 value.store(0, std::memory_order_relaxed);
74 }
75 for (auto& value : extra_values_) {
76 value.store(0, std::memory_order_relaxed);
77 }
78 count_.store(0, std::memory_order_release);
79 }
80
81 Percentile(const Percentile<M, Counter, ExtraBuckets, ExtraBucketSize>& other) noexcept { *this = other; }
82
83 Percentile& operator=(const Percentile& rhs) noexcept {
84 if (this == &rhs) {
85 return *this;
86 }
87
88 std::size_t sum = 0;
89 for (std::size_t i = 0; i < values_.size(); i++) {
90 const auto value = rhs.values_[i].load(std::memory_order_relaxed);
91 values_[i].store(value, std::memory_order_relaxed);
92 sum += value;
93 }
94
95 for (std::size_t i = 0; i < extra_values_.size(); i++) {
96 const auto value = rhs.extra_values_[i].load(std::memory_order_relaxed);
97 extra_values_[i].store(value, std::memory_order_relaxed);
98 sum += value;
99 }
100
101 count_ = sum;
102 return *this;
103 }
104
105 /// @brief Account for another value.
106 ///
107 /// `1` is added to the bucket corresponding to `value`
108 void Account(std::size_t value) noexcept {
109 if (value < values_.size()) {
110 values_[value].fetch_add(1, std::memory_order_relaxed);
111 } else {
112 if (!extra_values_.empty()) {
113 std::size_t extra_bucket = (value - values_.size() + ExtraBucketSize / 2) / ExtraBucketSize;
114 if (extra_bucket >= extra_values_.size()) {
115 extra_bucket = extra_values_.size() - 1;
116 }
117 extra_values_[extra_bucket].fetch_add(1, std::memory_order_relaxed);
118 } else {
119 values_.back().fetch_add(1, std::memory_order_relaxed);
120 }
121 }
122 count_.fetch_add(1, std::memory_order_release);
123 }
124
125 /// @brief Get X percentile - min value P so that total number
126 /// of elements in buckets is no less than X percent.
127 ///
128 /// @param percent - value in [0..100] - requested percentile.
129 /// If outside of 100, then returns last bucket that has any element in it.
130 std::size_t GetPercentile(double percent) const {
131 if (count_ == 0) {
132 return 0;
133 }
134
135 std::size_t sum = 0;
136 const std::size_t want_sum = count_.load(std::memory_order_acquire) * percent;
137 std::size_t max_value = 0;
138 for (std::size_t i = 0; i < values_.size(); i++) {
139 const auto value = values_[i].load(std::memory_order_relaxed);
140 sum += value;
141 if (sum * 100 > want_sum) {
142 return i;
143 }
144
145 if (value) {
146 max_value = i;
147 }
148 }
149
150 for (size_t i = 0; i < extra_values_.size(); i++) {
151 const auto value = extra_values_[i].load(std::memory_order_relaxed);
152 sum += value;
153 if (sum * 100 > want_sum) {
154 return ExtraBucketToValue(i);
155 }
156
157 if (value) {
158 max_value = ExtraBucketToValue(i);
159 }
160 }
161
162 return max_value;
163 }
164
165 template <class Duration = std::chrono::seconds>
166 void Add(
167 const Percentile<M, Counter, ExtraBuckets, ExtraBucketSize>& other,
168 [[maybe_unused]] Duration this_epoch_duration = Duration(),
169 [[maybe_unused]] Duration before_this_epoch_duration = Duration()
170 ) {
171 size_t sum = 0;
172 for (size_t i = 0; i < values_.size(); i++) {
173 const auto value = other.values_[i].load(std::memory_order_relaxed);
174 sum += value;
175 values_[i].fetch_add(value, std::memory_order_relaxed);
176 }
177
178 for (size_t i = 0; i < extra_values_.size(); i++) {
179 const auto value = other.extra_values_[i].load(std::memory_order_relaxed);
180 sum += value;
181 extra_values_[i].fetch_add(value, std::memory_order_relaxed);
182 }
183 count_.fetch_add(sum, std::memory_order_release);
184 }
185
186 /// @brief Zero out all the buckets and total number of elements.
187 void Reset() noexcept {
188 for (auto& value : values_) {
189 value.store(0, std::memory_order_relaxed);
190 }
191 for (auto& value : extra_values_) {
192 value.store(0, std::memory_order_relaxed);
193 }
194 count_ = 0;
195 }
196
197 /// @brief Total number of elements
198 Counter Count() const noexcept { return count_; }
199
200private:
201 size_t ExtraBucketToValue(std::size_t bucket) const noexcept { return values_.size() + bucket * ExtraBucketSize; }
202
203 static_assert(
204 std::atomic<Counter>::is_always_lock_free,
205 "`std::atomic<Counter>` is not lock-free. Please choose some "
206 "other `Counter` type"
207 );
208
209 std::array<std::atomic<Counter>, M> values_;
210 std::array<std::atomic<Counter>, ExtraBuckets> extra_values_;
211 std::atomic<Counter> count_;
212};
213
214std::string GetPercentileFieldName(double perc);
215
216template <size_t M, typename Counter, size_t ExtraBuckets, size_t ExtraBucketSize>
217void DumpMetric(
218 Writer& writer,
219 const Percentile<M, Counter, ExtraBuckets, ExtraBucketSize>& perc,
220 std::initializer_list<double> percents = {0, 50, 90, 95, 98, 99, 99.6, 99.9, 100}
221) {
222 for (const double percent : percents) {
223 writer
224 .ValueWithLabels(perc.GetPercentile(percent), {"percentile", statistics::GetPercentileFieldName(percent)});
225 }
226}
227
228} // namespace utils::statistics
229
230USERVER_NAMESPACE_END