/// Helper to create and initialize a SEARCH instance. Internally asserts that all fields are /// correctly initialized. /// /// Returns the initialized SEARCH instance, the value that `bin_duration` was initialized to and /// the current simulated time. fn init_search(initial_rtt: Duration) -> (Search, Duration, Instant) { letmut search = Search::new(); letmut now = now(); let rtt_est = RttEstimate::new(initial_rtt);
assert_eq!(
search.bin_duration(),
bin_duration, "bin_duration is initialized during first ACK and should not be reset by the new RTT"
);
assert_ne!(
search.curr_idx(),
Some(0), "curr_idx has progressed and wasn't reset to 0 by the second ACK"
);
}
#[test] #[cfg(debug_assertions)] #[should_panic(
expected = "bin_duration must be non-zero for correctness and to guard against div by zero -- initial_rtt was zero or too small"
)] fn initialization_with_zero_rtt() {
init_search(Duration::ZERO);
}
assert_eq!(search.curr_idx(), Some(2), "Should've skipped index 1");
assert_eq!(
search.acked_bin(1),
prev_acked, "bin 1 should be propagated from the previous value"
);
assert_eq!(
search.sent_bin(1),
prev_sent, "bin 1 should be propagated from the previous value"
);
assert_eq!(search.acked_bin(2), 2 * MIN_INITIAL_PACKET_SIZE);
assert_eq!(search.sent_bin(2), 2 * MIN_INITIAL_PACKET_SIZE);
}
// Pass 10 bins, which is the W SEARCH parameter for how many bins are in a window, // which is used as the guard for resetting if passing more than that.
now += 10 * bin_duration;
search.on_packet_sent(1, MIN_INITIAL_PACKET_SIZE);
ack(
&mut search,
&RttEstimate::new(INITIAL_RTT), 1,
MIN_INITIAL_PACKET_SIZE,
INITIAL_CWND,
now,
);
assert!(
search.curr_idx().is_some(), "passing one window of bins should not reset"
);
// Pass 11 bins, which is one bin more than the W SEARCH parameter for how many bins are in // a window.
now += 11 * bin_duration;
search.on_packet_sent(2, MIN_INITIAL_PACKET_SIZE); letmut cc_stats = CongestionControlStats::default();
search.record_acked_bytes(MIN_INITIAL_PACKET_SIZE);
search.on_packets_acked(
&RttEstimate::new(INITIAL_RTT), 2,
INITIAL_CWND,
&mut cc_stats,
now,
);
assert!(
search.curr_idx().is_none(), "passing more than one window of bins should reset"
);
assert_eq!(cc_stats.search_reset.count, 1);
assert_eq!(cc_stats.search_reset.max_passed_bins, Some(11));
let new_bin_duration = HIGH_RTT * 350 / 100 / 10;
assert_eq!(
search.curr_idx(),
Some(0), "curr_idx should be re-initialized after next ACK"
);
assert_eq!(
search.bin_duration(),
new_bin_duration, "bin_duration should be re-initialized with the new RTT"
);
assert_eq!(
search.bin_end(),
Some(now + new_bin_duration), "bin_end should be re-initialized with the new RTT"
);
// Trigger a second reset with more skipped bins to verify max_passed_bins tracks the max.
now += 15 * new_bin_duration;
search.on_packet_sent(4, MIN_INITIAL_PACKET_SIZE);
search.record_acked_bytes(MIN_INITIAL_PACKET_SIZE);
search.on_packets_acked(
&RttEstimate::new(HIGH_RTT), 4,
INITIAL_CWND,
&mut cc_stats,
now,
);
assert!(search.curr_idx().is_none());
assert_eq!(cc_stats.search_reset.count, 2);
assert_eq!(cc_stats.search_reset.max_passed_bins, Some(15));
}
// 10ms pass, not enough to reach bin_end
now += Duration::from_millis(10);
ack(
&mut search,
&RttEstimate::new(INITIAL_RTT), 1,
MIN_INITIAL_PACKET_SIZE,
INITIAL_CWND,
now,
);
assert_eq!(search.curr_idx(), Some(0));
// 10ms more pass, still not enough
now += Duration::from_millis(10);
ack(
&mut search,
&RttEstimate::new(INITIAL_RTT), 2,
MIN_INITIAL_PACKET_SIZE,
INITIAL_CWND,
now,
);
assert_eq!(search.curr_idx(), Some(0));
// another 20ms pass, now bins get updated
now += Duration::from_millis(20);
ack(
&mut search,
&RttEstimate::new(INITIAL_RTT), 3,
MIN_INITIAL_PACKET_SIZE,
INITIAL_CWND,
now,
);
// Assert that the bins got updated and that the bytes from all sent and acked packets are // accounted for (1 each from init_search and 3 each from the code above)
assert_eq!(search.curr_idx(), Some(1));
assert_eq!(search.acked_bin(1), 4 * MIN_INITIAL_PACKET_SIZE);
assert_eq!(search.sent_bin(1), 4 * MIN_INITIAL_PACKET_SIZE);
}
// Progress time so we have some space to look back for the test's sake.
now += bin_duration * 5;
search.on_packet_sent(1, MIN_INITIAL_PACKET_SIZE);
ack(
&mut search,
&RttEstimate::new(INITIAL_RTT), 1,
MIN_INITIAL_PACKET_SIZE,
INITIAL_CWND,
now,
); let curr_idx = search.curr_idx().unwrap(); let (prev_idx, fraction) = search.calc_prev_idx_test(INITIAL_RTT, curr_idx);
// With `rtt = 100ms` and `bin_duration = 35ms` we have passed `100 / 35 = 2.857` bins. Since // the value is floored we should get `prev_idx = curr_idx - 2` and `fraction = 85`.
assert_eq!(
prev_idx,
curr_idx - 2, "prev_idx should be `curr_idx - 2` and curr_idx is {curr_idx}"
);
assert_eq!(fraction, 85);
}
// Send and ack some packets to fill the bins. We advance one bin boundary per loop, so this // will give us exactly `previous_bin + 1000` bytes in each new bin for both sent and acked // bins. for pn in0..20 {
search.on_packet_sent(pn, 1000);
now += bin_duration + Duration::from_nanos(1);
ack(
&mut search,
&RttEstimate::new(INITIAL_RTT),
pn, 1000,
INITIAL_CWND,
now,
);
}
let curr_idx = search.curr_idx().unwrap(); let (prev_idx, fraction) = search.calc_prev_idx_test(INITIAL_RTT, curr_idx);
let sent_bytes = search.compute_sent_test(prev_idx - 10, prev_idx, fraction); let delv_bytes = search.compute_delv_test(curr_idx - 10, curr_idx);
// We looked back exactly 10 bins and each bin grew by 1000 bytes, so the results for both // `sent_bytes` and `delv_bytes` should be `10 * 1000 = 10_000`.
assert!(
sent_bytes == delv_bytes && delv_bytes == 10_000, "Should have `sent_bytes == delv_bytes == 10_000` and got `sent_bytes = {sent_bytes}` and `delv_bytes = {delv_bytes}`"
);
}
// With `rtt = 100ms` and `bin_duration = 35ms` we pass 2 bins per rtt. SEARCH checks start once // `curr_idx - passed_bins > W`. So with `W = 10` we need until `curr_idx = 12` for SEARCH // checks to run. while search.curr_idx() < Some(12) {
search.on_packet_sent(pn, bytes_this_round);
now += INITIAL_RTT;
ack(
&mut search,
&RttEstimate::new(INITIAL_RTT),
pn,
bytes_this_round,
bytes_this_round,
now,
);
pn += 1; // bytes double every round in slow start
bytes_this_round *= 2;
}
// Now keep sending and ack-ing for a bit with warm-up done to confirm that SEARCH doesn't exit // if the delivery rate is steady. for _ in1..=10 {
search.on_packet_sent(pn, bytes_this_round);
now += INITIAL_RTT; let result = ack(
&mut search,
&RttEstimate::new(INITIAL_RTT),
pn,
bytes_this_round,
bytes_this_round,
now,
);
pn += 1;
bytes_this_round *= 2;
assert!(
result.is_none(), "SEARCH should not exit if delivery rate is steady"
);
}
// Finally keep sending but only ack a quarter of the bytes sent. Eventually SEARCH should // detect an exit based on the flattening delivery rate.
search.on_packet_sent(pn, bytes_this_round);
now += INITIAL_RTT; let result = ack(
&mut search,
&RttEstimate::new(INITIAL_RTT),
pn,
bytes_this_round / 4,
bytes_this_round,
now,
);
assert_eq!(
result, None, "SEARCH doesn't immediately exit when delivery slows down"
);
pn += 1;
bytes_this_round += bytes_this_round / 4;
// Pass a persistent `cc_stats` to `on_packets_acked` to verify stats are recorded on exit. letmut cc_stats = CongestionControlStats::default();
search.on_packet_sent(pn, bytes_this_round);
now += INITIAL_RTT;
search.record_acked_bytes(bytes_this_round / 4); let result = search.on_packets_acked(
&RttEstimate::new(INITIAL_RTT),
pn,
bytes_this_round,
&mut cc_stats,
now,
);
assert_eq!(
result,
Some(bytes_this_round), "Once slow down is not just intermittent SEARCH exits"
);
assert!(cc_stats.search_empty_buffer_target.is_some());
assert!(cc_stats.search_full_buffer_target.is_some());
}
#[test] fn inflated_rtt_is_guarded() { let (mut search, bin_duration, mut now) = init_search(INITIAL_RTT); let rtt_est = RttEstimate::new(INITIAL_RTT); letmut pn = 1;
// Advance to curr_idx >= 28 so that with 600ms RTT: // bins_last_rtt = 600ms / 35ms = 17, so prev_idx = curr_idx - 17 // prev_idx = 28 - 17 = 11 > W(10) --> first guard passes // curr_idx - prev_idx = 17 >= EXTRA_BINS(15) --> second guard fires while search.curr_idx() < Some(28) {
search.on_packet_sent(pn, MIN_INITIAL_PACKET_SIZE);
now += bin_duration + Duration::from_nanos(1);
ack(
&mut search,
&rtt_est,
pn,
MIN_INITIAL_PACKET_SIZE,
INITIAL_CWND,
now,
);
pn += 1;
}
let curr_idx = search.curr_idx().unwrap(); let high_rtt = Duration::from_millis(600);
assert_eq!(
search.evaluate_test(high_rtt, curr_idx, INITIAL_CWND),
Outcome::RttInflated(17),
);
// Verify the stat is recorded through the full on_packets_acked path. letmut cc_stats = CongestionControlStats::default();
search.on_packet_sent(pn, MIN_INITIAL_PACKET_SIZE);
now += bin_duration + Duration::from_nanos(1);
search.record_acked_bytes(MIN_INITIAL_PACKET_SIZE);
search.on_packets_acked(
&RttEstimate::new(high_rtt),
pn,
INITIAL_CWND,
&mut cc_stats,
now,
);
assert_eq!(cc_stats.search_lookback_bins_needed, Some(17));
// A lower RTT that still triggers RttInflated should not overwrite the max. // curr_idx is now 29. With 525ms RTT: bins_last_rtt = floor(525/35) = 15, // prev_idx = 29 - 15 = 14 > W(10), curr_idx - prev_idx = 15 >= EXTRA_BINS(15) → RttInflated.
pn += 1; let lower_rtt = Duration::from_millis(525);
search.on_packet_sent(pn, MIN_INITIAL_PACKET_SIZE);
now += bin_duration + Duration::from_nanos(1);
search.record_acked_bytes(MIN_INITIAL_PACKET_SIZE);
search.on_packets_acked(
&RttEstimate::new(lower_rtt),
pn,
INITIAL_CWND,
&mut cc_stats,
now,
);
assert_eq!(cc_stats.search_lookback_bins_needed, Some(17));
}
#[test] fn no_sent_bytes() { let (mut search, bin_duration, mut now) = init_search(INITIAL_RTT); let rtt_est = RttEstimate::new(INITIAL_RTT); letmut pn = 1;
// After init_search, never call on_packet_sent again. All subsequent bins are // stamped with the same cumulative sent_bytes value, making compute_sent return 0. // Advance to curr_idx >= 13 so prev_idx = curr_idx - 2 > W(10). while search.curr_idx() < Some(13) {
now += bin_duration + Duration::from_nanos(1);
ack(&mut search, &rtt_est, pn, 0, INITIAL_CWND, now);
pn += 1;
}
let curr_idx = search.curr_idx().unwrap();
assert_eq!(
search.evaluate_test(INITIAL_RTT, curr_idx, INITIAL_CWND),
Outcome::ZeroSent,
);
// Verify the stat is recorded through the full on_packets_acked path. letmut cc_stats = CongestionControlStats::default();
now += bin_duration + Duration::from_nanos(1);
search.record_acked_bytes(0);
search.on_packets_acked(&rtt_est, pn, INITIAL_CWND, &mut cc_stats, now);
assert_eq!(cc_stats.search_zero_sent_bytes, 1);
}
#[test] fn warming_up() { let (mut search, bin_duration, mut now) = init_search(INITIAL_RTT); let rtt_est = RttEstimate::new(INITIAL_RTT); letmut pn = 1;
// Advance to curr_idx = 12, the exact WarmingUp boundary. // bins_last_rtt = 100ms / 35ms = 2, so prev_idx = 12 - 2 = 10 = W(10). // Guard is prev_idx <= W, so this is the last index that returns WarmingUp. while search.curr_idx() < Some(12) {
search.on_packet_sent(pn, MIN_INITIAL_PACKET_SIZE);
now += bin_duration + Duration::from_nanos(1);
ack(
&mut search,
&rtt_est,
pn,
MIN_INITIAL_PACKET_SIZE,
INITIAL_CWND,
now,
);
pn += 1;
}
let curr_idx = search.curr_idx().unwrap();
assert_eq!(
search.evaluate_test(INITIAL_RTT, curr_idx, INITIAL_CWND),
Outcome::WarmingUp,
);
// One more bin crosses the boundary: prev_idx = 13 - 2 = 11 > W(10).
search.on_packet_sent(pn, MIN_INITIAL_PACKET_SIZE);
now += bin_duration + Duration::from_nanos(1);
ack(
&mut search,
&rtt_est,
pn,
MIN_INITIAL_PACKET_SIZE,
INITIAL_CWND,
now,
);
let curr_idx = search.curr_idx().unwrap(); // Now the SEARCH checks should run.
assert_eq!(
search.evaluate_test(INITIAL_RTT, curr_idx, INITIAL_CWND),
Outcome::Continue(0),
);
}
#[test] fn continue_when_delivery_rate_steady() { let (mut search, bin_duration, mut now) = init_search(INITIAL_RTT); let rtt_est = RttEstimate::new(INITIAL_RTT); letmut pn = 1;
// Advance past warm-up boundary with equal send/ack each bin. // With W = 10 and bin_duration = 100ms / 35ms = 2 we need to advance to curr_idx = 12. while search.curr_idx() < Some(13) {
search.on_packet_sent(pn, MIN_INITIAL_PACKET_SIZE);
now += bin_duration + Duration::from_nanos(1);
ack(
&mut search,
&rtt_est,
pn,
MIN_INITIAL_PACKET_SIZE,
INITIAL_CWND,
now,
);
pn += 1;
}
// Keep going for 10 more bins with steady delivery rate, asserting // Continue each time. for _ in0..10 {
search.on_packet_sent(pn, MIN_INITIAL_PACKET_SIZE);
now += bin_duration + Duration::from_nanos(1);
ack(
&mut search,
&rtt_est,
pn,
MIN_INITIAL_PACKET_SIZE,
INITIAL_CWND,
now,
);
pn += 1;
// Verify the stat is recorded and tracks the running max. // Ack less than sent to create a non-zero norm_diff. letmut cc_stats = CongestionControlStats::default();
search.on_packet_sent(pn, MIN_INITIAL_PACKET_SIZE);
now += bin_duration + Duration::from_nanos(1);
search.record_acked_bytes(MIN_INITIAL_PACKET_SIZE / 4);
search.on_packets_acked(&rtt_est, pn, INITIAL_CWND, &mut cc_stats, now); let max = cc_stats.search_max_norm_diff;
assert!(max > Some(0));
// A subsequent steady round should not overwrite the max.
pn += 1;
search.on_packet_sent(pn, MIN_INITIAL_PACKET_SIZE);
now += bin_duration + Duration::from_nanos(1);
search.record_acked_bytes(MIN_INITIAL_PACKET_SIZE);
search.on_packets_acked(&rtt_est, pn, INITIAL_CWND, &mut cc_stats, now);
assert_eq!(cc_stats.search_max_norm_diff, max);
}
#[test] fn first_and_second_rtt_stats() { usecrate::cc::classic_cc::SlowStart as _;
// After a reset, two subsequent ACKs with a distinct RTT must not overwrite either recorded // value.
search.reset();
for pn in2..=3 {
search.on_packet_sent(pn, MIN_INITIAL_PACKET_SIZE);
now += POST_RESET_RTT;
search.record_acked_bytes(MIN_INITIAL_PACKET_SIZE);
search.on_packets_acked(
&RttEstimate::new(POST_RESET_RTT),
pn,
INITIAL_CWND,
&mut cc_stats,
now,
);
}
// Assert that the recorded RTTs remain unchanged after the reset.
assert_eq!(cc_stats.search_first_rtt, Some(INITIAL_RTT));
assert_eq!(cc_stats.search_second_rtt, Some(LOW_RTT));
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.