apollo_framework/average.rs
1//! Provides a helper which computes a average of a series of values.
2//!
3//! This is intended to be used when measuring the system performance which is often tracked as
4//! averages (e.g. execution duration of commands).
5//!
6//! An [Average](Average) is internally mutable without needing a mutable reference as we rely on
7//! atomic intrinsics as provided by modern processors / compilers.
8//!
9//! # Example
10//!
11//! ```
12//! # use apollo_framework::average::Average;
13//! let avg = Average::new();
14//! avg.add(10);
15//! avg.add(20);
16//! avg.add(30);
17//!
18//! assert_eq!(avg.avg(), 20);
19//! assert_eq!(avg.count(), 3);
20//! ```
21use crate::fmt::format_micros;
22use std::fmt;
23use std::fmt::Display;
24use std::sync::atomic::{AtomicU64, Ordering};
25
26/// Computes a sliding average of a series of values.
27///
28/// This is intended to record performance measurements and to keep track of the sliding average
29/// as well as the total number of recorded values.
30///
31/// Note that this class overflows gracefully.
32///
33/// # Example
34///
35/// ```
36/// # use apollo_framework::average::Average;
37/// let avg = Average::new();
38/// avg.add(10);
39/// avg.add(20);
40/// avg.add(30);
41///
42/// assert_eq!(avg.avg(), 20);
43/// assert_eq!(avg.count(), 3);
44/// ```
45#[derive(Default)]
46pub struct Average {
47 sum_and_count: AtomicU64,
48 count: AtomicU64,
49}
50
51impl Clone for Average {
52 fn clone(&self) -> Self {
53 Average {
54 sum_and_count: AtomicU64::new(self.sum_and_count.load(Ordering::Relaxed)),
55 count: AtomicU64::new(self.count.load(Ordering::Relaxed)),
56 }
57 }
58}
59
60impl Average {
61 /// Creates a new average.
62 pub fn new() -> Average {
63 Average {
64 sum_and_count: AtomicU64::new(0),
65 count: AtomicU64::new(0),
66 }
67 }
68
69 fn sum_and_count(&self) -> (i32, i32) {
70 let last_sum_and_count = self.sum_and_count.load(Ordering::Relaxed);
71 let count = (last_sum_and_count & 0xFFFFFFFF) as i32;
72 let sum = ((last_sum_and_count >> 32) & 0xFFFFFFFF) as i32;
73
74 (sum, count)
75 }
76
77 /// Adds another value to be added to the average calculation.
78 ///
79 /// Internally we simply update the global u64 counter to keep track of the total recorded
80 /// values. Additionally, we have another u64 which is split into two i32 fields. One of these
81 /// is used to keep the actual count of the sliding average and another is used to store the
82 /// sum of the values.
83 ///
84 /// Whenever we recorded 100 values or the sum counter might overflow, we divide both values
85 /// by two and add the new values. This yields a sliding average which is fit for our purposes.
86 ///
87 /// As the main task is to store the average duration of a task in microseconds, the i32 sum
88 /// field shouldn't overflow under normal conditions.
89 ///
90 /// We perform this trickery (splitting a single field into two) so that this algorithm is
91 /// completely lock and wait free, as we only utilize atomic load and store operations. This
92 /// guarantees correctness while ensuring maximal performance.
93 pub fn add(&self, value: i32) {
94 let _ = self.count.fetch_add(1, Ordering::Relaxed);
95
96 let (mut sum, mut count) = self.sum_and_count();
97
98 while count > 100 || sum as i64 + value as i64 > i32::MAX as i64 {
99 sum = count / 2 * sum / count;
100 count /= 2;
101 }
102
103 sum += value;
104 count += 1;
105
106 let next_sum_and_count = (sum as u64 & 0xFFFFFFFF) << 32 | (count as u64 & 0xFFFFFFFF);
107 self.sum_and_count
108 .store(next_sum_and_count, Ordering::Relaxed);
109 }
110
111 /// Returns the total number of recorded values (unless an overflow of the internal u64 counter
112 /// occurred).
113 pub fn count(&self) -> u64 {
114 self.count.load(Ordering::Relaxed)
115 }
116
117 /// Computes the sliding average of the last 100 values.
118 pub fn avg(&self) -> i32 {
119 let (sum, count) = self.sum_and_count();
120
121 if sum == 0 {
122 0
123 } else {
124 sum / count
125 }
126 }
127}
128
129impl Display for Average {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 format_micros(self.avg(), f)?;
132 write!(f, " ({})", self.count())?;
133 Ok(())
134 }
135}
136
137#[cfg(test)]
138mod test {
139 use crate::average::Average;
140
141 #[test]
142 fn empty_average_is_properly_initialized() {
143 let avg = Average::new();
144 assert_eq!(avg.avg(), 0);
145 assert_eq!(avg.count(), 0);
146 }
147
148 #[test]
149 fn average_with_some_values_works() {
150 let avg = Average::new();
151 for i in 1..=10 {
152 avg.add(i);
153 }
154 assert_eq!(avg.avg(), 5);
155 assert_eq!(avg.count(), 10);
156 }
157
158 #[test]
159 fn formatting_average_works() {
160 let avg = Average::new();
161 avg.add(10_123);
162 assert_eq!(format!("{}", avg), "10.1 ms (1)");
163 }
164
165 #[test]
166 fn average_with_many_values_keeps_count() {
167 let avg = Average::new();
168 for i in 1..=1000 {
169 avg.add(i);
170 }
171 assert_eq!(avg.avg(), 928);
172 assert_eq!(avg.count(), 1000);
173 }
174
175 #[test]
176 fn average_overflows_sanely() {
177 {
178 let avg = Average::new();
179 avg.add(i32::MAX);
180 assert_eq!(avg.avg(), i32::MAX);
181 avg.add(i32::MAX);
182 assert_eq!(avg.avg(), i32::MAX);
183 avg.add(i32::MAX / 2);
184 avg.add(i32::MAX / 2);
185 assert_eq!(avg.avg(), i32::MAX / 2);
186 }
187 {
188 let avg = Average::new();
189 avg.add(10);
190 avg.add(i32::MAX - 50);
191 avg.add(60);
192
193 // If an overflow occurs, we compute the internal average (in this case the average of
194 // 10 and std::i32::MAX - 50. We then accept the next value (60) and compute the average
195 // between these two as result...
196 let average_before_overflow = (i32::MAX - 50 + 10) / 2;
197 let expected_average = (average_before_overflow + 60) / 2;
198 assert_eq!(avg.avg(), expected_average);
199 }
200 }
201}