userver: userver/utils/statistics/recentperiod.hpp Source File
Loading...
Searching...
No Matches
recentperiod.hpp
1#pragma once
2
3#include <atomic>
4#include <chrono>
5#include <tuple>
6#include <type_traits>
7#include <vector>
8
9#include <userver/utils/datetime.hpp>
10#include <userver/utils/statistics/fwd.hpp>
11#include <userver/utils/statistics/recentperiod_detail.hpp>
12
13USERVER_NAMESPACE_BEGIN
14
15namespace utils::statistics {
16
17/** \brief Class maintains circular buffer of Counters
18 *
19 * At any time current Counter is accessible for modification via
20 * GetCurrentCounter().
21 * Counter can provide a Reset() member function to clear contents.
22 * @see utils::statistics::Percentile
23 */
24template <typename Counter, typename Result,
25 typename Timer = utils::datetime::SteadyClock>
26class RecentPeriod {
27 public:
28 using Duration = typename Timer::duration;
29
30 static_assert(
31 (detail::kResultWantsAddFunction<Result, Counter, Duration> ||
32 detail::kResultCanUseAddAssign<Result, Counter>),
33 "The Result template type argument must provide either Add(Counter, "
34 "Duration, Duration) function or add assignment operator");
35
36 static constexpr bool kUseAddFunction =
37 detail::kResultWantsAddFunction<Result, Counter, Duration>;
38
39 /**
40 * @param epoch_duration duration of epoch.
41 * @param max_duration max duration to calculate statistics for
42 * must be multiple of epoch_duration.
43 */
44 RecentPeriod(Duration epoch_duration = std::chrono::seconds(5),
45 Duration max_duration = std::chrono::seconds(60))
48 epoch_index_(0),
50
51 Counter& GetCurrentCounter() { return items_[get_current_index()].counter; }
52
53 Counter& GetPreviousCounter(int epochs_ago) {
54 return items_[get_previous_index(epochs_ago)].counter;
55 }
56
57 /** \brief Aggregates counters within given time range
58 *
59 * @param duration Time range. Special value Duration::min() -> use
60 * whole RecentPeriod range.
61 * @param with_current_epoch Include current (possibly unfinished) counter
62 * into aggregation
63 *
64 * Type Result must have method Add(Counter, Duration, Duration) or allow
65 * addition of counter values
66 */
67 // NOLINTNEXTLINE(readability-const-return-type)
68 const Result GetStatsForPeriod(Duration duration = Duration::min(),
69 bool with_current_epoch = false) const {
70 if (duration == Duration::min()) {
71 duration = max_duration_;
72 }
73
74 Result result{};
75 Duration now = Timer::now().time_since_epoch();
76 Duration current_epoch = get_epoch_for_duration(now);
77 Duration start_epoch = current_epoch - duration;
78 Duration first_epoch_duration = now - current_epoch;
79 size_t index = epoch_index_.load();
80
81 for (size_t i = 0; i < items_.size();
82 i++, index = (index + items_.size() - 1) % items_.size()) {
83 Duration epoch = items_[index].epoch;
84
85 if (epoch > current_epoch) continue;
86 if (epoch == current_epoch && !with_current_epoch) continue;
87 if (epoch < start_epoch) break;
88
89 if constexpr (kUseAddFunction) {
90 Duration this_epoch_duration =
91 (i == 0) ? first_epoch_duration : epoch_duration_;
92
93 Duration before_this_epoch_duration = epoch - start_epoch;
94 result.Add(items_[index].counter, this_epoch_duration,
95 before_this_epoch_duration);
96 } else {
97 result += items_[index].counter;
98 }
99 }
100
101 return result;
102 }
103
104 Duration GetEpochDuration() const { return epoch_duration_; }
105
106 Duration GetMaxDuration() const { return max_duration_; }
107
108 void UpdateEpochIfOld() { std::ignore = get_current_index(); }
109
110 void Reset() {
111 for (auto& item : items_) {
112 item.Reset();
113 }
114 }
115
116 private:
117 size_t get_current_index() const {
118 while (true) {
119 Duration now = Timer::now().time_since_epoch();
120 Duration epoch = get_epoch_for_duration(now);
121 size_t index = epoch_index_.load();
122 Duration bucket_epoch = items_[index].epoch.load();
123
124 if (epoch != bucket_epoch) {
125 size_t new_index = (index + 1) % items_.size();
126
127 if (epoch_index_.compare_exchange_weak(index, new_index)) {
128 items_[new_index].epoch = epoch;
129 items_[(new_index + 1) % items_.size()].Reset();
130 return new_index;
131 }
132 } else {
133 return index;
134 }
135 }
136 }
137
138 size_t get_previous_index(int epochs_ago) {
139 int index = static_cast<int>(get_current_index()) - epochs_ago;
140 while (index < 0) index += items_.size();
141 return index % items_.size();
142 }
143
144 Duration get_epoch_for_duration(Duration duration) const {
145 auto now = std::chrono::duration_cast<Duration>(duration);
146 return now - now % epoch_duration_;
147 }
148
149 static size_t get_size_for_duration(Duration epoch_duration,
150 Duration max_duration) {
151 /* 3 = current bucket, next zero bucket and extra one to handle
152 possible race. */
153 return max_duration.count() / epoch_duration.count() + 3;
154 }
155
156 struct EpochBucket {
157 static constexpr bool kUseReset = detail::kCanReset<Counter>;
158 std::atomic<Duration> epoch;
159 Counter counter;
160
161 EpochBucket() { Reset(); }
162
163 void Reset() {
164 epoch = Duration::min();
165 if constexpr (kUseReset) {
166 counter.Reset();
167 } else {
168 counter = 0;
169 }
170 }
171 };
172
173 const Duration epoch_duration_;
174 const Duration max_duration_;
175 mutable std::atomic_size_t epoch_index_;
176 mutable std::vector<EpochBucket> items_;
177};
178
179/// @a Writer support for @a RecentPeriod
180template <typename Counter, typename Result, typename Timer>
181void DumpMetric(Writer& writer,
182 const RecentPeriod<Counter, Result, Timer>& recent_period) {
183 writer = recent_period.GetStatsForPeriod();
184}
185
186/// Reset support for @a RecentPeriod
187template <typename Counter, typename Result, typename Timer>
188void ResetMetric(RecentPeriod<Counter, Result, Timer>& recent_period) {
189 recent_period.Reset();
190}
191
192} // namespace utils::statistics
193
194USERVER_NAMESPACE_END