| /* |
| * Copyright (c) 2026 The WebRTC project authors. All Rights Reserved. |
| * |
| * Use of this source code is governed by a BSD-style license |
| * that can be found in the LICENSE file in the root of the source |
| * tree. An additional intellectual property rights grant can be found |
| * in the file PATENTS. All contributing project authors may |
| * be found in the AUTHORS file in the root of the source tree. |
| */ |
| #include "modules/congestion_controller/ect1_policy.h" |
| |
| #include "api/units/time_delta.h" |
| #include "api/units/timestamp.h" |
| #include "test/gtest.h" |
| |
| namespace webrtc { |
| namespace { |
| |
| constexpr TimeDelta kTimeoutBeforeAck = TimeDelta::Millis(500); |
| constexpr TimeDelta kTimeoutAfterAck = TimeDelta::Seconds(5); |
| constexpr TimeDelta kRetryDelay = TimeDelta::Seconds(10); |
| // Shorter than the largest gap that counts as time spent sending, so that |
| // every interval counts in full. |
| constexpr TimeDelta kSendInterval = TimeDelta::Millis(100); |
| constexpr Timestamp kStartTime = Timestamp::Seconds(1000); |
| // When sending ECT(1) from kStartTime without feedback stops the marking. |
| constexpr Timestamp kDropDetectedTime = |
| kStartTime + kTimeoutBeforeAck + kSendInterval; |
| |
| Ect1Policy CreateEnabledPolicy() { |
| Ect1Policy policy; |
| policy.SetFeedbackSupportsEcn(true); |
| policy.SetCongestionControllerSupportsEcn(true); |
| return policy; |
| } |
| |
| // Sends an ECT(1) packet every kSendInterval in [start, end]. |
| void SendEct1Packets(Timestamp start, Timestamp end, Ect1Policy& policy) { |
| for (Timestamp now = start; now <= end; now += kSendInterval) { |
| policy.OnPacketSent(now, /*sent_as_ect1=*/true); |
| } |
| } |
| |
| Ect1Policy CreatePolicyThatDetectedDroppedEct1() { |
| Ect1Policy policy = CreateEnabledPolicy(); |
| SendEct1Packets(kStartTime, kDropDetectedTime, policy); |
| return policy; |
| } |
| |
| TEST(Ect1PolicyTest, DoesNotSendEct1ByDefault) { |
| Ect1Policy policy; |
| EXPECT_FALSE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, SendsEct1IfFeedbackAndCongestionControllerSupportEcn) { |
| EXPECT_TRUE(CreateEnabledPolicy().ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, DoesNotSendEct1IfFeedbackDoesNotSupportEcn) { |
| Ect1Policy policy; |
| policy.SetFeedbackSupportsEcn(false); |
| policy.SetCongestionControllerSupportsEcn(true); |
| EXPECT_FALSE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, DoesNotSendEct1IfCongestionControllerDoesNotSupportEcn) { |
| Ect1Policy policy; |
| policy.SetFeedbackSupportsEcn(true); |
| policy.SetCongestionControllerSupportsEcn(false); |
| EXPECT_FALSE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, StopsSendingEct1IfFeedbackReportsBleaching) { |
| Ect1Policy policy = CreateEnabledPolicy(); |
| policy.OnPacketsFeedback(/*has_bleached_ect1=*/true, |
| /*has_preserved_ect1=*/false); |
| EXPECT_FALSE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, KeepsSendingEct1IfFeedbackReportsNoBleaching) { |
| Ect1Policy policy = CreateEnabledPolicy(); |
| policy.OnPacketsFeedback(/*has_bleached_ect1=*/false, |
| /*has_preserved_ect1=*/false); |
| EXPECT_TRUE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, RouteChangeReEnablesEct1AfterBleaching) { |
| Ect1Policy policy = CreateEnabledPolicy(); |
| policy.OnPacketsFeedback(/*has_bleached_ect1=*/true, |
| /*has_preserved_ect1=*/true); |
| ASSERT_FALSE(policy.ShouldSendEct1()); |
| |
| policy.OnNetworkRouteChanged(); |
| EXPECT_TRUE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, RenegotiationDoesNotReEnableEct1AfterBleaching) { |
| Ect1Policy policy = CreateEnabledPolicy(); |
| policy.OnPacketsFeedback(/*has_bleached_ect1=*/true, |
| /*has_preserved_ect1=*/true); |
| ASSERT_FALSE(policy.ShouldSendEct1()); |
| |
| policy.SetFeedbackSupportsEcn(true); |
| EXPECT_FALSE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, StopsSendingEct1IfNoFeedbackWhileSendingEct1) { |
| Ect1Policy policy = CreateEnabledPolicy(); |
| Timestamp last_send = kStartTime + kTimeoutBeforeAck; |
| SendEct1Packets(kStartTime, last_send, policy); |
| EXPECT_TRUE(policy.ShouldSendEct1()); |
| |
| // One more packet takes the time spent sending past the timeout. |
| policy.OnPacketSent(last_send + kSendInterval, /*sent_as_ect1=*/true); |
| EXPECT_FALSE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, PacketsSentAsNotEctDoNotStopSendingEct1) { |
| Ect1Policy policy = CreateEnabledPolicy(); |
| for (Timestamp now = kStartTime; now <= kStartTime + TimeDelta::Seconds(30); |
| now += kSendInterval) { |
| policy.OnPacketSent(now, /*sent_as_ect1=*/false); |
| } |
| EXPECT_TRUE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, KeepsSendingEct1WhileTheMarkingIsPreserved) { |
| Ect1Policy policy = CreateEnabledPolicy(); |
| Timestamp now = kStartTime; |
| for (int i = 0; i < 100; ++i) { |
| policy.OnPacketSent(now, /*sent_as_ect1=*/true); |
| policy.OnPacketsFeedback(/*has_bleached_ect1=*/false, |
| /*has_preserved_ect1=*/true); |
| now += kSendInterval; |
| } |
| EXPECT_TRUE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, FeedbackForPacketsSentBeforeEct1KeepsEct1Enabled) { |
| // Just after ECT(1) is enabled, feedback still reports packets that were |
| // sent as Not-ECT. It proves that packets reach the receiver, so it must |
| // stop the timeout just like feedback that acknowledges an ECT(1) packet. |
| Ect1Policy policy = CreateEnabledPolicy(); |
| Timestamp last_send = kStartTime + kTimeoutBeforeAck; |
| SendEct1Packets(kStartTime, last_send, policy); |
| policy.OnPacketsFeedback(/*has_bleached_ect1=*/false, |
| /*has_preserved_ect1=*/false); |
| |
| // Without the feedback this packet would stop the marking. |
| policy.OnPacketSent(last_send + kSendInterval, /*sent_as_ect1=*/true); |
| EXPECT_TRUE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, UsesLongerTimeoutAfterTheMarkingIsPreserved) { |
| Ect1Policy policy = CreateEnabledPolicy(); |
| policy.OnPacketSent(kStartTime, /*sent_as_ect1=*/true); |
| policy.OnPacketsFeedback(/*has_bleached_ect1=*/false, |
| /*has_preserved_ect1=*/true); |
| |
| // Sending past the short timeout is no longer enough. |
| Timestamp start = kStartTime + kSendInterval; |
| SendEct1Packets(start, start + kTimeoutBeforeAck + kSendInterval, policy); |
| EXPECT_TRUE(policy.ShouldSendEct1()); |
| |
| // Sending past the long timeout is. |
| SendEct1Packets(start + kTimeoutBeforeAck + 2 * kSendInterval, |
| start + kTimeoutAfterAck + kSendInterval, policy); |
| EXPECT_FALSE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, DoesNotStopSendingEct1AfterAPauseInSending) { |
| // Nothing can be concluded from a period where nothing was sent, however |
| // long it is. |
| Ect1Policy policy = CreateEnabledPolicy(); |
| policy.OnPacketSent(kStartTime, /*sent_as_ect1=*/true); |
| policy.OnPacketSent(kStartTime + TimeDelta::Seconds(30), |
| /*sent_as_ect1=*/true); |
| EXPECT_TRUE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, DoesNotStopSendingEct1AfterAPauseFollowingALostPacket) { |
| Ect1Policy policy = CreateEnabledPolicy(); |
| // ECT(1) is known to work on this route. |
| policy.OnPacketSent(kStartTime, /*sent_as_ect1=*/true); |
| policy.OnPacketsFeedback(/*has_bleached_ect1=*/false, |
| /*has_preserved_ect1=*/true); |
| |
| // A packet is sent and lost to ordinary loss, then sending pauses for 5s. |
| // That is longer than the timeout but says nothing about the path. |
| Timestamp last_send = kStartTime + kSendInterval; |
| policy.OnPacketSent(last_send, /*sent_as_ect1=*/true); |
| policy.OnPacketSent(last_send + TimeDelta::Seconds(5), |
| /*sent_as_ect1=*/true); |
| EXPECT_TRUE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, StopsSendingEct1WhenCongestedAndNoFeedbackArrives) { |
| // Without feedback the congestion window fills up and the pacer only sends |
| // a packet every kCongestedPacketInterval. Detection must still happen. |
| Ect1Policy policy = CreateEnabledPolicy(); |
| Timestamp now = kStartTime; |
| for (int i = 0; i < 10 && policy.ShouldSendEct1(); ++i) { |
| policy.OnPacketSent(now, /*sent_as_ect1=*/true); |
| now += TimeDelta::Millis(500); |
| } |
| EXPECT_FALSE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, RouteChangeReEnablesEct1AfterEct1PacketsWereDropped) { |
| Ect1Policy policy = CreatePolicyThatDetectedDroppedEct1(); |
| ASSERT_FALSE(policy.ShouldSendEct1()); |
| |
| policy.OnNetworkRouteChanged(); |
| EXPECT_TRUE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, RetriesEct1AfterTheRetryDelay) { |
| Ect1Policy policy = CreatePolicyThatDetectedDroppedEct1(); |
| ASSERT_FALSE(policy.ShouldSendEct1()); |
| // Sending continues as Not-ECT while ECT(1) is disabled. |
| policy.OnPacketSent(kDropDetectedTime + kRetryDelay + kSendInterval, |
| /*sent_as_ect1=*/false); |
| EXPECT_TRUE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, DoesNotRetryEct1BeforeTheRetryDelay) { |
| Ect1Policy policy = CreatePolicyThatDetectedDroppedEct1(); |
| ASSERT_FALSE(policy.ShouldSendEct1()); |
| policy.OnPacketSent(kDropDetectedTime + kRetryDelay - kSendInterval, |
| /*sent_as_ect1=*/false); |
| EXPECT_FALSE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, RetriesEct1OnlyOncePerRoute) { |
| Ect1Policy policy = CreatePolicyThatDetectedDroppedEct1(); |
| ASSERT_FALSE(policy.ShouldSendEct1()); |
| Timestamp retry_time = kDropDetectedTime + kRetryDelay + kSendInterval; |
| policy.OnPacketSent(retry_time, /*sent_as_ect1=*/false); |
| ASSERT_TRUE(policy.ShouldSendEct1()); |
| |
| // The retry finds that the packets are still dropped. |
| Timestamp detected_again = retry_time + kTimeoutBeforeAck + kSendInterval; |
| SendEct1Packets(retry_time, detected_again, policy); |
| ASSERT_FALSE(policy.ShouldSendEct1()); |
| |
| policy.OnPacketSent(detected_again + kRetryDelay + kSendInterval, |
| /*sent_as_ect1=*/false); |
| EXPECT_FALSE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, RouteChangeAllowsANewRetry) { |
| Ect1Policy policy = CreatePolicyThatDetectedDroppedEct1(); |
| ASSERT_FALSE(policy.ShouldSendEct1()); |
| |
| policy.OnNetworkRouteChanged(); |
| |
| Timestamp start = kDropDetectedTime + kSendInterval; |
| Timestamp detected_again = start + kTimeoutBeforeAck + kSendInterval; |
| SendEct1Packets(start, detected_again, policy); |
| ASSERT_FALSE(policy.ShouldSendEct1()); |
| |
| policy.OnPacketSent(detected_again + kRetryDelay + kSendInterval, |
| /*sent_as_ect1=*/false); |
| EXPECT_TRUE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, DoesNotRetryEct1AfterBleaching) { |
| Ect1Policy policy = CreateEnabledPolicy(); |
| policy.OnPacketsFeedback(/*has_bleached_ect1=*/true, |
| /*has_preserved_ect1=*/true); |
| ASSERT_FALSE(policy.ShouldSendEct1()); |
| |
| policy.OnPacketSent(kStartTime + kRetryDelay + kSendInterval, |
| /*sent_as_ect1=*/false); |
| EXPECT_FALSE(policy.ShouldSendEct1()); |
| } |
| |
| TEST(Ect1PolicyTest, RouteChangeRestoresTheShortTimeout) { |
| Ect1Policy policy = CreateEnabledPolicy(); |
| policy.OnPacketSent(kStartTime, /*sent_as_ect1=*/true); |
| policy.OnPacketsFeedback(/*has_bleached_ect1=*/false, |
| /*has_preserved_ect1=*/true); |
| policy.OnNetworkRouteChanged(); |
| |
| SendEct1Packets(kStartTime, kDropDetectedTime, policy); |
| EXPECT_FALSE(policy.ShouldSendEct1()); |
| } |
| |
| } // namespace |
| } // namespace webrtc |