1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "cast/streaming/bandwidth_estimator.h"
6
7 #include <chrono>
8 #include <limits>
9 #include <random>
10
11 #include "gmock/gmock.h"
12 #include "gtest/gtest.h"
13 #include "platform/api/time.h"
14 #include "util/chrono_helpers.h"
15
16 namespace openscreen {
17 namespace cast {
18 namespace {
19
20 using openscreen::operator<<; // For std::chrono::duration gtest pretty-print.
21
22 // BandwidthEstimator configuration common to all tests.
23 constexpr int kMaxPacketsPerTimeslice = 10;
24 constexpr Clock::duration kTimesliceDuration = milliseconds(10);
25 constexpr int kTimeslicesPerSecond = seconds(1) / kTimesliceDuration;
26
27 // Use a fake, fixed start time.
28 constexpr Clock::time_point kStartTime =
29 Clock::time_point() + Clock::duration(1234567890);
30
31 // Range of "fuzz" to add to timestamps in BandwidthEstimatorTest::AddFuzz().
32 constexpr Clock::duration kMaxFuzzOffset = milliseconds(15);
33 constexpr int kFuzzLowerBoundClockTicks = (-kMaxFuzzOffset).count();
34 constexpr int kFuzzUpperBoundClockTicks = kMaxFuzzOffset.count();
35
36 class BandwidthEstimatorTest : public testing::Test {
37 public:
BandwidthEstimatorTest()38 BandwidthEstimatorTest()
39 : estimator_(kMaxPacketsPerTimeslice, kTimesliceDuration, kStartTime) {}
40
estimator()41 BandwidthEstimator* estimator() { return &estimator_; }
42
43 // Returns |t| plus or minus |kMaxFuzzOffset|.
AddFuzz(Clock::time_point t)44 Clock::time_point AddFuzz(Clock::time_point t) {
45 return t + Clock::duration(distribution_(rand_));
46 }
47
48 private:
49 BandwidthEstimator estimator_;
50
51 // These are used to generate random values for AddFuzz().
52 static constexpr std::minstd_rand::result_type kRandSeed =
53 kStartTime.time_since_epoch().count();
54 std::minstd_rand rand_{kRandSeed};
55 std::uniform_int_distribution<int> distribution_{kFuzzLowerBoundClockTicks,
56 kFuzzUpperBoundClockTicks};
57 };
58
59 // Tests that, without any data, there won't be any estimates.
TEST_F(BandwidthEstimatorTest,DoesNotEstimateWithoutAnyInput)60 TEST_F(BandwidthEstimatorTest, DoesNotEstimateWithoutAnyInput) {
61 EXPECT_EQ(0, estimator()->ComputeNetworkBandwidth());
62 }
63
64 // Tests the case where packets are being sent, but the Receiver hasn't provided
65 // feedback (e.g., due to a network blackout).
TEST_F(BandwidthEstimatorTest,DoesNotEstimateWithoutFeedback)66 TEST_F(BandwidthEstimatorTest, DoesNotEstimateWithoutFeedback) {
67 Clock::time_point now = kStartTime;
68 for (int i = 0; i < 3; ++i) {
69 const Clock::time_point end = now + estimator()->history_window();
70 for (; now < end; now += kTimesliceDuration) {
71 estimator()->OnBurstComplete(i, now);
72 EXPECT_EQ(0, estimator()->ComputeNetworkBandwidth());
73 }
74 now = end;
75 }
76 }
77
78 // Tests the case where packets are being sent, and a connection to the Receiver
79 // has been confirmed (because RTCP packets are coming in), but the Receiver has
80 // not successfully received anything.
TEST_F(BandwidthEstimatorTest,DoesNotEstimateIfNothingSuccessfullyReceived)81 TEST_F(BandwidthEstimatorTest, DoesNotEstimateIfNothingSuccessfullyReceived) {
82 const Clock::duration kRoundTripTime = milliseconds(1);
83
84 Clock::time_point now = kStartTime;
85 for (int i = 0; i < 3; ++i) {
86 const Clock::time_point end = now + estimator()->history_window();
87 for (; now < end; now += kTimesliceDuration) {
88 estimator()->OnBurstComplete(i, now);
89 estimator()->OnRtcpReceived(now + kRoundTripTime, kRoundTripTime);
90 EXPECT_EQ(0, estimator()->ComputeNetworkBandwidth());
91 }
92 now = end;
93 }
94 }
95
96 // Tests that, when the Receiver successfully receives the payload bytes at a
97 // fixed rate, the network bandwidth estimates are based on the amount of time
98 // the Sender spent transmitting.
TEST_F(BandwidthEstimatorTest,EstimatesAtVariousBurstSaturations)99 TEST_F(BandwidthEstimatorTest, EstimatesAtVariousBurstSaturations) {
100 // These determine how many packets to burst in the simulation below.
101 constexpr int kDivisors[] = {
102 1, // Burst 100% of max possible packets.
103 2, // Burst 50% of max possible packets.
104 5, // Burst 20% of max possible packets.
105 };
106
107 const Clock::duration kRoundTripTime = milliseconds(1);
108
109 constexpr int kReceivedBytesPerSecond = 256000;
110 constexpr int kReceivedBytesPerTimeslice =
111 kReceivedBytesPerSecond / kTimeslicesPerSecond;
112 static_assert(kReceivedBytesPerSecond % kTimeslicesPerSecond == 0,
113 "Test expectations won't account for rounding errors.");
114
115 ASSERT_EQ(0, estimator()->ComputeNetworkBandwidth());
116
117 // Simulate bursting at various rates, and confirm the bandwidth estimate is
118 // increasing for each burst rate. The estimate should be increasing because
119 // the total time spent transmitting is decreasing (while the same number of
120 // bytes are being received).
121 Clock::time_point now = kStartTime;
122 for (int divisor : kDivisors) {
123 SCOPED_TRACE(testing::Message() << "divisor=" << divisor);
124
125 const Clock::time_point end = now + estimator()->history_window();
126 for (; now < end; now += kTimesliceDuration) {
127 estimator()->OnBurstComplete(kMaxPacketsPerTimeslice / divisor, now);
128 const Clock::time_point rtcp_arrival_time = now + kRoundTripTime;
129 estimator()->OnPayloadReceived(kReceivedBytesPerTimeslice,
130 rtcp_arrival_time, kRoundTripTime);
131 estimator()->OnRtcpReceived(rtcp_arrival_time, kRoundTripTime);
132 }
133 now = end;
134
135 const int estimate = estimator()->ComputeNetworkBandwidth();
136 EXPECT_EQ(divisor * kReceivedBytesPerSecond * CHAR_BIT, estimate);
137 }
138 }
139
140 // Tests that magnitude of the network round trip times, as well as random
141 // variance in packet arrival times, do not have a significant effect on the
142 // bandwidth estimates.
TEST_F(BandwidthEstimatorTest,EstimatesIndependentOfFeedbackDelays)143 TEST_F(BandwidthEstimatorTest, EstimatesIndependentOfFeedbackDelays) {
144 constexpr int kFactor = 2;
145 constexpr int kPacketsPerBurst = kMaxPacketsPerTimeslice / kFactor;
146 static_assert(kMaxPacketsPerTimeslice % kFactor == 0, "wanted exactly half");
147
148 constexpr milliseconds kRoundTripTimes[3] = {milliseconds(1), milliseconds(9),
149 milliseconds(42)};
150
151 constexpr int kReceivedBytesPerSecond = 2000000;
152 constexpr int kReceivedBytesPerTimeslice =
153 kReceivedBytesPerSecond / kTimeslicesPerSecond;
154
155 // An arbitrary threshold. Sources of error include anything that would place
156 // byte flows outside the history window (e.g., AddFuzz(), or the 42ms round
157 // trip time).
158 constexpr int kMaxErrorPercent = 3;
159
160 Clock::time_point now = kStartTime;
161 for (Clock::duration round_trip_time : kRoundTripTimes) {
162 SCOPED_TRACE(testing::Message()
163 << "round_trip_time=" << round_trip_time.count());
164
165 const Clock::time_point end = now + estimator()->history_window();
166 for (; now < end; now += kTimesliceDuration) {
167 estimator()->OnBurstComplete(kPacketsPerBurst, now);
168 const Clock::time_point rtcp_arrival_time =
169 AddFuzz(now + round_trip_time);
170 estimator()->OnPayloadReceived(kReceivedBytesPerTimeslice,
171 rtcp_arrival_time, round_trip_time);
172 estimator()->OnRtcpReceived(rtcp_arrival_time, round_trip_time);
173 }
174 now = end;
175
176 constexpr int kExpectedEstimate =
177 kFactor * kReceivedBytesPerSecond * CHAR_BIT;
178 constexpr int kMaxError = kExpectedEstimate * kMaxErrorPercent / 100;
179 EXPECT_NEAR(kExpectedEstimate, estimator()->ComputeNetworkBandwidth(),
180 kMaxError);
181 }
182 }
183
184 // Tests that both the history tracking internal to BandwidthEstimator, as well
185 // as its computation of the bandwidth estimate, are both resistant to possible
186 // integer overflow cases. The internal implementation always clamps to the
187 // valid range of int.
TEST_F(BandwidthEstimatorTest,ClampsEstimateToMaxInt)188 TEST_F(BandwidthEstimatorTest, ClampsEstimateToMaxInt) {
189 constexpr int kPacketsPerBurst = kMaxPacketsPerTimeslice / 5;
190 static_assert(kMaxPacketsPerTimeslice % 5 == 0, "wanted exactly 20%");
191 const Clock::duration kRoundTripTime = milliseconds(1);
192
193 int last_estimate = estimator()->ComputeNetworkBandwidth();
194 ASSERT_EQ(last_estimate, 0);
195
196 // Simulate increasing numbers of bytes received per timeslice until it
197 // reaches values near INT_MAX. Along the way, the bandwidth estimates
198 // themselves should start clamping and, because of the fuzz added to RTCP
199 // arrival times, individual buckets in BandwidthEstimator::FlowTracker will
200 // occassionally be clamped too.
201 Clock::time_point now = kStartTime;
202 for (unsigned int bytes_received_per_timeslice = 1;
203 // Not overflowed past INT_MAX.
204 static_cast<int>(bytes_received_per_timeslice) > 0;
205 bytes_received_per_timeslice *= 2) {
206 int int_bytes = static_cast<int>(bytes_received_per_timeslice);
207 SCOPED_TRACE(testing::Message()
208 << "bytes_received_per_timeslice=" << int_bytes);
209
210 const Clock::time_point end = now + estimator()->history_window() / 4;
211 for (; now < end; now += kTimesliceDuration) {
212 estimator()->OnBurstComplete(kPacketsPerBurst, now);
213 const Clock::time_point rtcp_arrival_time = AddFuzz(now + kRoundTripTime);
214 estimator()->OnPayloadReceived(int_bytes, rtcp_arrival_time,
215 kRoundTripTime);
216 estimator()->OnRtcpReceived(rtcp_arrival_time, kRoundTripTime);
217 }
218 now = end;
219
220 const int estimate = estimator()->ComputeNetworkBandwidth();
221 EXPECT_LE(last_estimate, estimate);
222 last_estimate = estimate;
223 }
224
225 // Confirm there was a loop iteration at which the estimate reached INT_MAX
226 // and then stayed there for successive loop iterations.
227 EXPECT_EQ(std::numeric_limits<int>::max(), last_estimate);
228 }
229
230 } // namespace
231 } // namespace cast
232 } // namespace openscreen
233