Skip to content

Commit 118cd9f

Browse files
Swap to using the same timeout estimator as TCP
1 parent 39b62ae commit 118cd9f

7 files changed

Lines changed: 449 additions & 45 deletions

File tree

src/CMakeLists.txt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,15 @@ configure_file(nuclear.in ${PROJECT_BINARY_DIR}/nuclear)
2828

2929
# Build the library
3030
find_package(Threads REQUIRED)
31-
file(GLOB_RECURSE src "*.c" "*.cpp" "*.hpp" "*.ipp")
31+
file(
32+
GLOB_RECURSE
33+
src
34+
CONFIGURE_DEPENDS
35+
"*.c"
36+
"*.cpp"
37+
"*.hpp"
38+
"*.ipp"
39+
)
3240
add_library(nuclear STATIC ${src})
3341
add_library(NUClear::nuclear ALIAS nuclear)
3442

src/extension/network/NUClearNetwork.cpp

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ namespace extension {
501501
if (ptr) {
502502

503503
auto now = std::chrono::steady_clock::now();
504-
auto timeout = it->last_send + ptr->round_trip_time;
504+
auto timeout = it->last_send + ptr->rtt.timeout();
505505

506506
// Check if we should have expected an ack by now for some packets
507507
if (timeout < now) {
@@ -510,7 +510,7 @@ namespace extension {
510510
it->last_send = now;
511511

512512
// The next time we should check for a timeout
513-
auto next_timeout = now + ptr->round_trip_time;
513+
auto next_timeout = now + ptr->rtt.timeout();
514514
if (next_timeout < next_event) {
515515
next_event = next_timeout;
516516
next_event_callback(next_event);
@@ -869,7 +869,7 @@ namespace extension {
869869
// Check for and delete any timed out packets
870870
for (auto it = assemblers.begin(); it != assemblers.end();) {
871871
const auto now = std::chrono::steady_clock::now();
872-
const auto timeout = remote->round_trip_time * 10.0;
872+
const auto timeout = remote->rtt.timeout() * 10.0;
873873
const auto& last_chunk_time = it->second.first;
874874

875875
it = now > last_chunk_time + timeout ? assemblers.erase(it) : std::next(it);
@@ -919,8 +919,7 @@ namespace extension {
919919

920920
// Approximate how long the round trip is to this remote so we can work out how
921921
// long before retransmitting
922-
// We use a baby kalman filter to help smooth out jitter
923-
remote->measure_round_trip(round_trip);
922+
remote->rtt.measure(round_trip);
924923

925924
// Update our acks
926925
bool all_acked = true;
@@ -987,7 +986,7 @@ namespace extension {
987986
s->last_send = std::chrono::steady_clock::now();
988987

989988
// The next time we should check for a timeout
990-
auto next_timeout = s->last_send + remote->round_trip_time;
989+
auto next_timeout = s->last_send + remote->rtt.timeout();
991990
if (next_timeout < next_event) {
992991
next_event = next_timeout;
993992
next_event_callback(next_event);
@@ -1108,7 +1107,7 @@ namespace extension {
11081107
queue.targets.emplace_back(it->second, acks);
11091108

11101109
// The next time we should check for a timeout
1111-
auto next_timeout = std::chrono::steady_clock::now() + it->second->round_trip_time;
1110+
auto next_timeout = std::chrono::steady_clock::now() + it->second->rtt.timeout();
11121111
if (next_timeout < next_event) {
11131112
next_event = next_timeout;
11141113
next_event_callback(next_event);

src/extension/network/NUClearNetwork.hpp

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
#include "../../util/network/sock_t.hpp"
4141
#include "../../util/platform.hpp"
42+
#include "RTTEstimator.hpp"
4243
#include "wire_protocol.hpp"
4344

4445
namespace NUClear {
@@ -79,41 +80,8 @@ namespace extension {
7980
std::pair<std::chrono::steady_clock::time_point, std::map<uint16_t, std::vector<uint8_t>>>>
8081
assemblers;
8182

82-
/// Struct storing the kalman filter for round trip time
83-
struct RoundTripKF {
84-
float process_noise = 1e-6f;
85-
float measurement_noise = 1e-1f;
86-
float variance = 1.0f;
87-
float mean = 1.0f;
88-
};
89-
/// A little kalman filter for estimating round trip time
90-
RoundTripKF round_trip_kf{};
91-
92-
std::chrono::steady_clock::duration round_trip_time{std::chrono::seconds(1)};
93-
94-
void measure_round_trip(std::chrono::steady_clock::duration time) {
95-
96-
// Make our measurement into a float seconds type
97-
const std::chrono::duration<float> m =
98-
std::chrono::duration_cast<std::chrono::duration<float>>(time);
99-
100-
// Alias variables
101-
const auto& Q = round_trip_kf.process_noise;
102-
const auto& R = round_trip_kf.measurement_noise;
103-
auto& P = round_trip_kf.variance;
104-
auto& X = round_trip_kf.mean;
105-
106-
// Calculate our kalman gain
107-
const float K = (P + Q) / (P + Q + R);
108-
109-
// Do filter
110-
P = R * (P + Q) / (R + P + Q);
111-
X = X + (m.count() - X) * K;
112-
113-
// Put result into our variable
114-
round_trip_time = std::chrono::duration_cast<std::chrono::steady_clock::duration>(
115-
std::chrono::duration<float>(X));
116-
}
83+
/// RTT estimator for this network target
84+
RTTEstimator rtt;
11785
};
11886

11987
NUClearNetwork() = default;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright (c) 2025 NUClear Contributors
5+
*
6+
* This file is part of the NUClear codebase.
7+
* See https://github.com/Fastcode/NUClear for further info.
8+
*
9+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
10+
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
11+
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
12+
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
13+
*
14+
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
15+
* Software.
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18+
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19+
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
20+
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
*/
22+
#include "RTTEstimator.hpp"
23+
24+
#include <cmath>
25+
26+
namespace NUClear {
27+
namespace extension {
28+
namespace network {
29+
30+
void RTTEstimator::measure(std::chrono::steady_clock::duration time) {
31+
// Convert measurement to float seconds
32+
const std::chrono::duration<float> m = std::chrono::duration_cast<std::chrono::duration<float>>(time);
33+
const float sample_rtt = m.count();
34+
35+
// Calculate RTT variation
36+
const float err = sample_rtt - smoothed_rtt;
37+
rtt_var = (1 - beta) * rtt_var + beta * std::abs(err);
38+
39+
// Update smoothed RTT
40+
smoothed_rtt = (1 - alpha) * smoothed_rtt + alpha * sample_rtt;
41+
42+
// Calculate RTO (smoothed RTT + 4 * RTT variation) and bound to limits
43+
rto = std::min(std::max(smoothed_rtt + 4 * rtt_var, min_rto), max_rto);
44+
}
45+
46+
std::chrono::steady_clock::duration RTTEstimator::timeout() const {
47+
return std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::duration<float>(rto));
48+
}
49+
50+
} // namespace network
51+
} // namespace extension
52+
} // namespace NUClear
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright (c) 2025 NUClear Contributors
5+
*
6+
* This file is part of the NUClear codebase.
7+
* See https://github.com/Fastcode/NUClear for further info.
8+
*
9+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
10+
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
11+
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
12+
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
13+
*
14+
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
15+
* Software.
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18+
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19+
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
20+
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
*/
22+
#ifndef NUCLEAR_EXTENSION_NETWORK_RTT_ESTIMATOR_HPP
23+
#define NUCLEAR_EXTENSION_NETWORK_RTT_ESTIMATOR_HPP
24+
25+
#include <chrono>
26+
#include <cstdint>
27+
#include <stdexcept>
28+
29+
namespace NUClear {
30+
namespace extension {
31+
namespace network {
32+
33+
/**
34+
* Implements TCP-style Round Trip Time (RTT) estimation using Jacobson/Karels algorithm.
35+
*
36+
* This class provides RTT estimation functionality similar to TCP's RTT estimation mechanism.
37+
* It uses an Exponentially Weighted Moving Average (EWMA) to smooth RTT measurements and
38+
* calculate a retransmission timeout (RTO) value. The implementation follows the TCP
39+
* Jacobson/Karels algorithm which provides robust RTT estimation that:
40+
* - Smoothly tracks the mean RTT
41+
* - Adapts to RTT variations
42+
* - Handles network jitter
43+
* - Provides conservative timeout values
44+
*/
45+
class RTTEstimator {
46+
public:
47+
/**
48+
* Construct a new RTT Estimator
49+
*
50+
* @param alpha Weight for RTT smoothing (default: 0.125, TCP standard)
51+
* @param beta Weight for RTT variation (default: 0.25, TCP standard)
52+
* @param initial_rtt Initial RTT estimate in seconds (default: 1.0)
53+
* @param initial_rtt_var Initial RTT variation in seconds (default: 0.0)
54+
* @param min_rto Minimum RTO value in seconds (default: 0.1)
55+
* @param max_rto Maximum RTO value in seconds (default: 60.0)
56+
*
57+
* The alpha and beta parameters control how quickly the estimator adapts to changes:
58+
* - alpha: Lower values (e.g. 0.125) make the smoothed RTT more stable but slower to adapt
59+
* - beta: Lower values (e.g. 0.25) make the RTT variation more stable but slower to adapt
60+
*
61+
* @throws std::invalid_argument if alpha or beta are not in range [0,1]
62+
* @throws std::invalid_argument if min_rto >= max_rto
63+
*/
64+
RTTEstimator(float alpha = 0.125f,
65+
float beta = 0.25f,
66+
float initial_rtt = 1.0f,
67+
float initial_rtt_var = 0.0f,
68+
float min_rto = 0.1f,
69+
float max_rto = 60.0f)
70+
: alpha(alpha)
71+
, beta(beta)
72+
, min_rto(min_rto)
73+
, max_rto(max_rto)
74+
, smoothed_rtt(initial_rtt)
75+
, rtt_var(initial_rtt_var)
76+
, rto(std::min(std::max(initial_rtt + 4 * initial_rtt_var, min_rto), max_rto)) {
77+
78+
if (alpha < 0.0f || alpha > 1.0f) {
79+
throw std::invalid_argument("alpha must be in range [0,1]");
80+
}
81+
if (beta < 0.0f || beta > 1.0f) {
82+
throw std::invalid_argument("beta must be in range [0,1]");
83+
}
84+
if (min_rto >= max_rto) {
85+
throw std::invalid_argument("min_rto must be less than max_rto");
86+
}
87+
}
88+
89+
/**
90+
* Update the RTT estimate with a new measurement
91+
*
92+
* Updates the smoothed RTT, RTT variation, and RTO using the Jacobson/Karels algorithm:
93+
* 1. RTT variation = (1 - beta) * old_variation + beta * |smoothed_rtt - new_rtt|
94+
* 2. Smoothed RTT = (1 - alpha) * old_rtt + alpha * new_rtt
95+
* 3. RTO = smoothed_rtt + 4 * rtt_var
96+
*
97+
* The RTO is bounded between min_rto and max_rto to prevent extreme values.
98+
*
99+
* @param time The measured round trip time
100+
*/
101+
void measure(std::chrono::steady_clock::duration time);
102+
103+
/**
104+
* Get the current retransmission timeout
105+
*
106+
* @return The RTO as a duration. This value represents the recommended timeout
107+
* for network operations based on the current RTT estimates.
108+
*/
109+
std::chrono::steady_clock::duration timeout() const;
110+
111+
private:
112+
float alpha; ///< Weight for RTT smoothing (typically 0.125)
113+
float beta; ///< Weight for RTT variation (typically 0.25)
114+
float min_rto; ///< Minimum RTO value in seconds
115+
float max_rto; ///< Maximum RTO value in seconds
116+
float smoothed_rtt; ///< Smoothed RTT estimate in seconds
117+
float rtt_var; ///< RTT variation in seconds
118+
float rto; ///< Retransmission timeout in seconds
119+
};
120+
121+
} // namespace network
122+
} // namespace extension
123+
} // namespace NUClear
124+
125+
#endif // NUCLEAR_EXTENSION_NETWORK_RTT_ESTIMATOR_HPP

tests/CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ set_target_properties(${catch2_targets} PROPERTIES CXX_CLANG_TIDY "")
4040
set_target_properties(${catch2_target} PROPERTIES CMAKE_CXX_FLAGS "")
4141

4242
# Create a test_util library that is used by all tests
43-
file(GLOB_RECURSE test_util_src "test_util/*.cpp")
43+
file(GLOB_RECURSE test_util_src CONFIGURE_DEPENDS "test_util/*.cpp")
4444
add_library(test_util OBJECT ${test_util_src})
4545
# This is linking WHOLE_ARCHIVE as otherwise the linker will remove the WSAHolder from the final binary
4646
# As a result the WSA initialisation code won't run and the network tests will fail
@@ -50,7 +50,7 @@ target_include_directories(test_util PUBLIC ${PROJECT_BINARY_DIR}/include ${PROJ
5050
target_include_directories(test_util PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
5151

5252
# Create a test binary for each test file
53-
file(GLOB_RECURSE test_sources "tests/*.cpp")
53+
file(GLOB_RECURSE test_sources CONFIGURE_DEPENDS "tests/*.cpp")
5454
foreach(test_file ${test_sources})
5555
get_filename_component(test_name ${test_file} NAME_WE)
5656
get_filename_component(test_dir ${test_file} DIRECTORY)

0 commit comments

Comments
 (0)