Skip to content

Commit 2e67c3d

Browse files
nadeaudiMongoDB Bot
authored andcommitted
SERVER-104814: Backport moving_average only
GitOrigin-RevId: 66b0bd2bf8155080f0e3c864d924b33f2b641c16
1 parent c1812c9 commit 2e67c3d

File tree

3 files changed

+280
-0
lines changed

3 files changed

+280
-0
lines changed

src/mongo/util/SConscript

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,16 @@ env.CppUnitTest(
233233
],
234234
)
235235

236+
env.CppUnitTest(
237+
target='moving_average_test',
238+
source=[
239+
'moving_average_test.cpp',
240+
],
241+
LIBDEPS=[
242+
'$BUILD_DIR/mongo/base',
243+
],
244+
)
245+
236246
env.Library(
237247
target='periodic_runner',
238248
source=[

src/mongo/util/moving_average.h

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* Copyright (C) 2025-present MongoDB, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*
17+
* As a special exception, the copyright holders give permission to link the
18+
* code of portions of this program with the OpenSSL library under certain
19+
* conditions as described in each individual source file and distribute
20+
* linked combinations including the program with the OpenSSL library. You
21+
* must comply with the Server Side Public License in all respects for
22+
* all of the code used other than as permitted herein. If you modify file(s)
23+
* with this exception, you may extend this exception to your version of the
24+
* file(s), but you are not obligated to do so. If you do not wish to do so,
25+
* delete this exception statement from your version. If you delete this
26+
* exception statement from all source files in the program, then also delete
27+
* it in the license file.
28+
*/
29+
30+
#pragma once
31+
32+
#include "mongo/platform/atomic.h"
33+
#include "mongo/util/assert_util.h"
34+
35+
#include <cmath>
36+
37+
#include <boost/optional.hpp>
38+
39+
namespace mongo {
40+
41+
/**
42+
* `MovingAverage` is an atomically updated [exponential moving average][1].
43+
*
44+
* A `MovingAverage` initially has no value. The `addSample(double)` member
45+
* function atomically contributes a value to the moving average. The first
46+
* call to `addSample` sets the initial value. Subsequent calls to `addSample`
47+
* combine the argument with the existing moving average. `addSample` returns
48+
* the new moving average value.
49+
*
50+
* The `get()` member function returns a snapshot of the value of the moving
51+
* average, or `boost::none` if `addSample` has never been called.
52+
*
53+
* `MovingAverage` has one required constructor parameter, the smoothing factor
54+
* `double alpha`, which determines the relative weight of new data versus the
55+
* previous average. `alpha` must satisfy `alpha > 0 && alpha < 1`. The member
56+
* function `alpha()` returns `alpha`.
57+
*
58+
* A typical choice for the smoothing factor is `0.2`. `0.2` is chosen per the
59+
* precedent set by the round-trip-time average calculation in [server
60+
* selection][2]. That specification notes:
61+
*
62+
* > A weighting factor of 0.2 was chosen to put about 85% of the weight of the
63+
* > average RTT on the 9 most recent observations.
64+
*
65+
* For use of `MovingAverage` as a server status metric, see
66+
* `moving_average_metric.h`.
67+
*
68+
* [1]: https://en.wikipedia.org/wiki/Exponential_smoothing
69+
* [2]:
70+
* https://github.com/mongodb/specifications/blob/master/source/server-selection/server-selection.md
71+
*/
72+
73+
class MovingAverage {
74+
public:
75+
/**
76+
* Creates an exponential moving average with smoothing factor alpha. The moving average
77+
* initially has no value. The behavior is undefined unless `alpha > 0 && alpha < 1`.
78+
*/
79+
explicit MovingAverage(double alpha) : _average(std::nan("")), _alpha(alpha) {
80+
invariant(_alpha > 0);
81+
invariant(_alpha < 1);
82+
}
83+
84+
/** Contributes the specified sample into the moving average. Returns the updated average. */
85+
double addSample(double sample) {
86+
double expected = _average.load();
87+
double desired;
88+
do {
89+
if (std::isnan(expected)) {
90+
desired = sample; // the very first sample
91+
} else {
92+
desired = _alpha * sample + (1 - _alpha) * expected;
93+
}
94+
} while (!_average.compareAndSwap(&expected, desired));
95+
return desired;
96+
}
97+
98+
/**
99+
* Returns the current moving average. If addSample has never been called on this object,
100+
* then returns `boost::none`.
101+
*/
102+
boost::optional<double> get() const {
103+
const double raw = _average.load();
104+
if (std::isnan(raw)) {
105+
return boost::none;
106+
}
107+
return raw;
108+
}
109+
110+
/**
111+
* Returns the smoothing factor of this moving average. It is the same value as specified in
112+
* the constructor.
113+
*/
114+
double alpha() const {
115+
return _alpha;
116+
}
117+
118+
private:
119+
Atomic<double> _average;
120+
const double _alpha;
121+
};
122+
123+
} // namespace mongo
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* Copyright (C) 2025-present MongoDB, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*
17+
* As a special exception, the copyright holders give permission to link the
18+
* code of portions of this program with the OpenSSL library under certain
19+
* conditions as described in each individual source file and distribute
20+
* linked combinations including the program with the OpenSSL library. You
21+
* must comply with the Server Side Public License in all respects for
22+
* all of the code used other than as permitted herein. If you modify file(s)
23+
* with this exception, you may extend this exception to your version of the
24+
* file(s), but you are not obligated to do so. If you do not wish to do so,
25+
* delete this exception statement from your version. If you delete this
26+
* exception statement from all source files in the program, then also delete
27+
* it in the license file.
28+
*/
29+
30+
#include "mongo/util/moving_average.h"
31+
32+
#include "mongo/unittest/barrier.h"
33+
#include "mongo/unittest/join_thread.h"
34+
#include "mongo/unittest/unittest.h"
35+
36+
#include <algorithm>
37+
#include <cmath>
38+
#include <numbers>
39+
#include <thread>
40+
#include <vector>
41+
42+
namespace mongo {
43+
namespace {
44+
45+
template <typename Func>
46+
void runForAlphas(Func&& func) {
47+
const double alphas[] = {0.05, 0.1, 0.2, 0.4, 0.8, 0.9, 0.95};
48+
for (const double alpha : alphas) {
49+
MovingAverage avg{alpha};
50+
func(avg);
51+
}
52+
}
53+
54+
// Verify that `MovingAverage::get()` returns `boost::none` if `addSample` has
55+
// never been called on the object.
56+
TEST(MovingAverageTest, StartsWithNone) {
57+
runForAlphas([](auto& avg) { ASSERT_EQ(avg.get(), boost::none) << " alpha=" << avg.alpha(); });
58+
}
59+
60+
// Verify that if `MovingAverage::addSample` has been called on the object only
61+
// once, then `get()` returns the value of that sample.
62+
TEST(MovingAverageTest, FirstSampleIsAverage) {
63+
const double first = -1.337;
64+
runForAlphas([=](auto& avg) {
65+
avg.addSample(first);
66+
ASSERT_EQ(avg.get(), first) << "alpha=" << avg.alpha();
67+
});
68+
}
69+
70+
// Verify that adding a sample to an exponential moving average results in a
71+
// new average that is between the previous average and the sample.
72+
// Sample from the sine function, for example.
73+
TEST(MovingAverageTest, AverageMovesTowardsSamples) {
74+
runForAlphas([](auto& avg) {
75+
double theta = 0;
76+
double sample = std::sin(theta);
77+
double oldAvg = avg.addSample(sample);
78+
const double delta = 0.1;
79+
theta += delta;
80+
do {
81+
sample = std::sin(theta);
82+
const double newAvg = avg.addSample(sample);
83+
const auto [below, above] = std::minmax(oldAvg, sample);
84+
85+
ASSERT_LTE(below, newAvg)
86+
<< "theta=" << theta << " sin(theta)=" << sample << " alpha=" << avg.alpha();
87+
ASSERT_GTE(above, newAvg)
88+
<< "theta=" << theta << " sin(theta)=" << sample << " alpha=" << avg.alpha();
89+
90+
oldAvg = newAvg;
91+
theta += delta;
92+
} while (theta < 2 * std::numbers::pi);
93+
});
94+
}
95+
96+
// Verify that `get()` returns the most recent average.
97+
TEST(MovingAverageTest, GetIsConsistentWithAddSampleAndIsIdempotent) {
98+
runForAlphas([](auto& avg) {
99+
// arbitrary history of samples
100+
const double warmup[] = {9898344, -309409, 2.7e-12, 42};
101+
102+
double mostRecentAvg;
103+
for (const double sample : warmup) {
104+
mostRecentAvg = avg.addSample(sample);
105+
}
106+
107+
ASSERT_EQ(avg.get(), mostRecentAvg) << "alpha=" << avg.alpha();
108+
ASSERT_EQ(avg.get(), mostRecentAvg) << "alpha=" << avg.alpha();
109+
});
110+
}
111+
112+
// Verify that two or more threads can concurrently call any combination of
113+
// `get()` and `addSample(...)` without upsetting code sanitizers like
114+
// ThreadSanitizer (tsan), AddressSansitizer (asan), and
115+
// UndefinedBehaviorSanitizer (ubsan). This test is only relevant when the test
116+
// driver is built with sanitizers (e.g. `--config=dbg_tsan` or
117+
// `--config=dbg_aubsan`).
118+
TEST(MovingAverageTest, ThreadSafe) {
119+
// At most as many threads as logical cores, or two threads if we don't
120+
// know the core count.
121+
const unsigned maxThreads = std::max(2u, std::thread::hardware_concurrency());
122+
123+
for (unsigned nThreads = 2; nThreads <= maxThreads; ++nThreads) {
124+
runForAlphas([=](auto& avg) {
125+
unittest::Barrier startingLine{nThreads};
126+
std::vector<unittest::JoinThread> threads;
127+
for (unsigned id = 0; id < nThreads; ++id) {
128+
threads.emplace_back([&, id]() {
129+
// Wait for the other threads to spawn.
130+
startingLine.countDownAndWait();
131+
132+
// Bang on `avg` for a while.
133+
double scratch = id;
134+
for (int i = 0; i < 1'000; ++i) {
135+
avg.addSample(scratch);
136+
const auto got = avg.get();
137+
ASSERT_NE(got, boost::none);
138+
scratch = *got;
139+
}
140+
});
141+
}
142+
});
143+
}
144+
}
145+
146+
} // namespace
147+
} // namespace mongo

0 commit comments

Comments
 (0)