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