@@ -762,30 +762,6 @@ TEST_F(CubicTest, Pacing_CongestionAvoidanceEstimation)
762762 ASSERT_EQ (Allowance, ExpectedAllowance);
763763}
764764
765- //
766- // Test: Pacing SendAllowance Capped at Available Window
767- // Scenario: When the pacing formula produces an allowance larger than the available
768- // congestion window (CW − BytesInFlight), it is capped to the available window.
769- // A large TimeSinceLastSend (1 second >> SmoothedRtt of 50ms) causes the pacing
770- // calculation to exceed the available space, exercising the cap.
771- //
772- TEST_F (CubicTest, Pacing_CappedAtAvailableWindow)
773- {
774- InitializeDefaultWithRtt (/* WindowPackets = */ 10 , /* HyStart = */ false );
775- Connection.Settings .PacingEnabled = TRUE ;
776-
777- // Send data
778- CC->QuicCongestionControlOnDataSent (CC, 1000 );
779-
780- // Large time delta: pacing formula produces (24640 * 1000000) / 50000 = 492800,
781- // which far exceeds available window (12320 - 1000 = 11320), triggering the cap.
782- uint32_t Allowance = CC->QuicCongestionControlGetSendAllowance (CC, 1000000 , TRUE );
783-
784- // Capped at available window
785- uint32_t AvailableWindow = Cubic->CongestionWindow - Cubic->BytesInFlight ;
786- ASSERT_EQ (Allowance, AvailableWindow);
787- }
788-
789765//
790766// Test: Congestion Avoidance AIMD vs CUBIC Window Selection
791767// Scenario: After loss triggers recovery and an ACK exits recovery, a subsequent
@@ -812,19 +788,35 @@ TEST_F(CubicTest, CongestionAvoidance_AIMDvsCubicSelection)
812788 // CUBIC formula and AIMD both execute, max() selects the winner.
813789 ASSERT_FALSE (Cubic->IsInRecovery );
814790
815- // Derivation of expected CW:
816- // KCubic = CubeRoot((24640/1232 * 3 << 9) / 4) = CubeRoot(7680) = 19
817- // KCubic = S_TO_MS(19) = 19000; KCubic >>= 3 = 2375
818- // TimeInCongAvoid = 1100000 - 1050000 = 50000 µs
819- // DeltaT = US_TO_MS(50000 - 2375000 + 50000) = -2275
820- // CubicWindow ≈ ((-2275²>>10) * -2275 * 492 >> 20) + 24640 ≈ 19245
821- // AimdWindow(17248) < CubicWindow(19245) → CUBIC path selected
822- // TargetWindow = max(17248, min(19245, 17248+8624)) = 19245
823- // Growth = (19245 - 17248) * 1232 / 17248 = 142
824- // Expected CW = 17248 + 142 = 17390
791+ // Reproduce the CUBIC formula to compute expected CubicWindow.
792+ // This mirrors the production code (OnCongestionEvent + OnDataAcknowledged).
793+ uint32_t WindowMax = Cubic->WindowMax ; // Set by EnterCongestionAvoidance
794+ uint32_t KCubic = CubeRoot ((WindowMax / DatagramPayloadLength * 3 << 9 ) / 4 );
795+ KCubic = KCubic * 1000 ; // S_TO_MS
796+ KCubic >>= 3 ;
797+
798+ uint64_t TimeInCongAvoidUs = CongAvoidAck.TimeNow - Cubic->TimeOfCongAvoidStart ;
799+ int64_t DeltaT =
800+ ((int64_t )TimeInCongAvoidUs -
801+ (int64_t )KCubic * 1000 + // MS_TO_US
802+ (int64_t )CongAvoidAck.SmoothedRtt ) / 1000 ; // US_TO_MS
803+
804+ int64_t CubicWindow =
805+ ((((DeltaT * DeltaT) >> 10 ) * DeltaT *
806+ (int64_t )(DatagramPayloadLength * 4 / 10 )) >> 20 ) +
807+ (int64_t )WindowMax;
808+
809+ // CUBIC > AIMD → bounded growth path selected
810+ ASSERT_GT (CubicWindow, (int64_t )Cubic->AimdWindow );
811+
812+ uint64_t TargetWindow = CXPLAT_MAX (
813+ WindowBeforeCongAvoid,
814+ CXPLAT_MIN ((uint64_t )CubicWindow, (uint64_t )WindowBeforeCongAvoid + (WindowBeforeCongAvoid >> 1 )));
815+ uint32_t ExpectedGrowth =
816+ (uint32_t )(((TargetWindow - WindowBeforeCongAvoid) * DatagramPayloadLength) / WindowBeforeCongAvoid);
817+
825818 ASSERT_GT (Cubic->CongestionWindow , WindowBeforeCongAvoid);
826- ASSERT_LE (Cubic->CongestionWindow , WindowBeforeCongAvoid + (WindowBeforeCongAvoid >> 1 ));
827- ASSERT_EQ (Cubic->CongestionWindow , WindowAfterLoss + (uint32_t )(((uint64_t )(19245 - WindowAfterLoss) * DatagramPayloadLength) / WindowAfterLoss));
819+ ASSERT_EQ (Cubic->CongestionWindow , WindowBeforeCongAvoid + ExpectedGrowth);
828820}
829821
830822//
@@ -1034,49 +1026,6 @@ TEST_F(CubicTest, CubicWindow_OverflowToBytesInFlightMax)
10341026 );
10351027}
10361028
1037- //
1038- // Test: Slow Start Window Growth and Send Allowance After ACK
1039- // Scenario: After filling the congestion window completely (blocked), an ACK frees
1040- // space and triggers slow-start growth. Verifies window grows by BytesAcked,
1041- // BytesInFlight decreases, and GetSendAllowance reflects the new available window.
1042- //
1043- TEST_F (CubicTest, SlowStart_WindowGrowthAndAllowance)
1044- {
1045- InitializeWithDefaults ();
1046- const uint16_t DatagramPayloadLength = QuicPathGetDatagramPayloadSize (&Connection.Paths [0 ]);
1047- // MTU=1280, payload = 1280 - 48 = 1232
1048- // InitialWindow = 10 * 1232 = 12320
1049- uint32_t InitialWindow = DatagramPayloadLength * Settings.InitialWindowPackets ;
1050- ASSERT_EQ (Cubic->CongestionWindow , InitialWindow);
1051-
1052- // Start with blocked state by filling the window
1053- CC->QuicCongestionControlOnDataSent (CC, Cubic->CongestionWindow );
1054- // BytesInFlight = 12320
1055-
1056- // Now free up space by acknowledging half the data
1057- uint32_t BytesAcked = InitialWindow / 2 ; // 6160
1058- QUIC_ACK_EVENT AckEvent = MakeAckEvent (1000000 , 5 , 10 , BytesAcked);
1059-
1060- CC->QuicCongestionControlOnDataAcknowledged (CC, &AckEvent);
1061-
1062- // After ACK in slow start:
1063- // - BytesInFlight = 12320 - 6160 = 6160
1064- // - Window grows by BytesAcked = 6160 (slow start)
1065- // - NewWindow = 12320 + 6160 = 18480
1066- uint32_t NewWindow = InitialWindow + BytesAcked;
1067- uint32_t BytesInFlightAfterAck = InitialWindow - BytesAcked;
1068- ASSERT_EQ (Cubic->CongestionWindow , NewWindow);
1069- ASSERT_EQ (Cubic->BytesInFlight , BytesInFlightAfterAck);
1070-
1071- // Note: CubicCongestionControlUpdateBlockedState is internal, but we can
1072- // test the logic through GetSendAllowance behavior changes
1073- // Pacing is NOT enabled, so Allowance = CongestionWindow - BytesInFlight
1074- uint32_t Allowance = CC->QuicCongestionControlGetSendAllowance (CC, 1000 , TRUE );
1075-
1076- uint32_t ExpectedAllowance = NewWindow - BytesInFlightAfterAck;
1077- ASSERT_EQ (Allowance, ExpectedAllowance);
1078- }
1079-
10801029//
10811030// Test: Spurious Congestion Event Rollback
10821031// Scenario: Tests the spurious congestion event handling. When a congestion event
@@ -1242,68 +1191,50 @@ TEST_F(CubicTest, HyStart_InitialStateVerification)
12421191//
12431192// Test: HyStart++ NOT_STARTED → DONE via Loss
12441193// Scenario: Tests direct transition from NOT_STARTED to DONE when packet loss
1245- // occurs before HyStart++ detection logic activates. This is the most common
1246- // path when network conditions cause loss during initial slow start .
1194+ // occurs before HyStart++ detection logic activates. Window reduction mechanics
1195+ // are verified by OnDataLost_WindowReduction; this test focuses on state transition .
12471196//
12481197TEST_F (CubicTest, HyStart_NotStartedToDone_ViaLoss)
12491198{
12501199 InitializeDefaultWithRtt (/* WindowPackets = */ 20 , /* HyStart = */ true );
12511200 // Precondition: Verify in NOT_STARTED state
12521201 ASSERT_EQ (Cubic->HyStartState , HYSTART_NOT_STARTED);
12531202
1254- uint32_t WindowBeforeLoss = Cubic->CongestionWindow ;
1255-
12561203 // Send data to have bytes in flight
12571204 CC->QuicCongestionControlOnDataSent (CC, 8000 );
12581205 Connection.Send .NextPacketNumber = 10 ;
12591206
1260- // Trigger loss event while still in NOT_STARTED
12611207 QUIC_LOSS_EVENT LossEvent = MakeLossEvent (2400 , 5 , 10 );
1262-
12631208 CC->QuicCongestionControlOnDataLost (CC, &LossEvent);
12641209
1265- // Postcondition: Should transition directly to DONE
1266- // After loss: Window = InitialWindow * 7 / 10, SSThresh = Window
1267- uint32_t ExpectedWindow = WindowBeforeLoss * 7 / 10 ;
12681210 ASSERT_EQ (Cubic->HyStartState , HYSTART_DONE);
1269- ASSERT_TRUE (Cubic->IsInRecovery );
1270- ASSERT_TRUE (Cubic->HasHadCongestionEvent );
1271- ASSERT_EQ (Cubic->CWndSlowStartGrowthDivisor , 1u ); // Reset to normal
1272- ASSERT_EQ (Cubic->CongestionWindow , ExpectedWindow);
1273- ASSERT_EQ (Cubic->SlowStartThreshold , ExpectedWindow);
1211+ ASSERT_EQ (Cubic->CWndSlowStartGrowthDivisor , 1u );
12741212}
12751213
12761214//
12771215// Test: HyStart++ NOT_STARTED → DONE via ECN
12781216// Scenario: Tests direct transition from NOT_STARTED to DONE when ECN marking
1279- // is received, indicating congestion before HyStart++ activates.
1217+ // is received. Window reduction mechanics are verified by OnEcn_CongestionSignal;
1218+ // this test focuses on state transition.
12801219//
12811220TEST_F (CubicTest, HyStart_NotStartedToDone_ViaECN)
12821221{
12831222 InitializeDefaultWithRtt (/* WindowPackets = */ 20 , /* HyStart = */ true );
12841223 // Precondition: Verify in NOT_STARTED state
12851224 ASSERT_EQ (Cubic->HyStartState , HYSTART_NOT_STARTED);
1286- uint32_t WindowBeforeECN = Cubic->CongestionWindow ;
12871225
12881226 // Send data
12891227 CC->QuicCongestionControlOnDataSent (CC, 8000 );
12901228 Connection.Send .NextPacketNumber = 15 ;
12911229
1292- // Trigger ECN event
12931230 QUIC_ECN_EVENT EcnEvent{};
12941231 EcnEvent.LargestPacketNumberAcked = 10 ;
12951232 EcnEvent.LargestSentPacketNumber = 15 ;
12961233
12971234 CC->QuicCongestionControlOnEcn (CC, &EcnEvent);
12981235
1299- // Postcondition: Should transition directly to DONE
1300- // After ECN: Window = InitialWindow * 7 / 10 (β = 0.7)
1301- uint32_t ExpectedWindow = WindowBeforeECN * 7 / 10 ;
13021236 ASSERT_EQ (Cubic->HyStartState , HYSTART_DONE);
1303- ASSERT_TRUE (Cubic->IsInRecovery );
1304- ASSERT_TRUE (Cubic->HasHadCongestionEvent );
13051237 ASSERT_EQ (Cubic->CWndSlowStartGrowthDivisor , 1u );
1306- ASSERT_EQ (Cubic->CongestionWindow , ExpectedWindow);
13071238}
13081239
13091240//
@@ -1513,61 +1444,6 @@ TEST_F(CubicTest, HyStart_StateInvariant_GrowthDivisor)
15131444 ASSERT_EQ (Cubic->CWndSlowStartGrowthDivisor , 1u );
15141445}
15151446
1516- //
1517- // Test: HyStart++ Multiple Congestion Events - State Stability
1518- // Scenario: Tests that multiple congestion events keep the state in DONE and
1519- // don't cause state corruption. Each event should trigger recovery logic but
1520- // state should remain DONE.
1521- //
1522- TEST_F (CubicTest, HyStart_MultipleCongestionEvents_StateStability)
1523- {
1524- InitializeDefaultWithRtt (/* WindowPackets = */ 30 , /* HyStart = */ true );
1525- const uint16_t DatagramPayloadLength = QuicPathGetDatagramPayloadSize (&Connection.Paths [0 ]);
1526- uint32_t InitialWindow = DatagramPayloadLength * Settings.InitialWindowPackets ;
1527-
1528- // First congestion event: NOT_STARTED → DONE
1529- CC->QuicCongestionControlOnDataSent (CC, 8000 );
1530- Connection.Send .NextPacketNumber = 10 ;
1531-
1532- QUIC_LOSS_EVENT FirstLoss = MakeLossEvent (2400 , 5 , 10 );
1533-
1534- CC->QuicCongestionControlOnDataLost (CC, &FirstLoss);
1535-
1536- ASSERT_EQ (Cubic->HyStartState , HYSTART_DONE);
1537- // After first loss: Window = InitialWindow * 7 / 10
1538- uint32_t WindowAfterFirst = InitialWindow * 7 / 10 ;
1539- ASSERT_EQ (Cubic->CongestionWindow , WindowAfterFirst);
1540-
1541- // Exit recovery
1542- Connection.Send .NextPacketNumber = 20 ;
1543- QUIC_ACK_EVENT RecoveryExitAck = MakeAckEvent (1100000 , 20 , 25 , 1200 , 50000 , 48000 );
1544-
1545- CC->QuicCongestionControlOnDataAcknowledged (CC, &RecoveryExitAck);
1546-
1547- // Second congestion event: DONE → DONE (should remain)
1548- CC->QuicCongestionControlOnDataSent (CC, 5000 );
1549- Connection.Send .NextPacketNumber = 30 ;
1550-
1551- QUIC_LOSS_EVENT SecondLoss = MakeLossEvent (1800 , 28 , 30 );
1552-
1553- CC->QuicCongestionControlOnDataLost (CC, &SecondLoss);
1554-
1555- ASSERT_EQ (Cubic->HyStartState , HYSTART_DONE); // Still DONE
1556- // After second loss: Window = WindowAfterFirst * 7 / 10
1557- uint32_t WindowAfterSecond = WindowAfterFirst * 7 / 10 ;
1558- ASSERT_EQ (Cubic->CongestionWindow , WindowAfterSecond);
1559-
1560- // Third congestion event via ECN: DONE → DONE
1561- Connection.Send .NextPacketNumber = 40 ;
1562- QUIC_ECN_EVENT EcnEvent{};
1563- EcnEvent.LargestPacketNumberAcked = 35 ;
1564- EcnEvent.LargestSentPacketNumber = 40 ;
1565-
1566- CC->QuicCongestionControlOnEcn (CC, &EcnEvent);
1567-
1568- ASSERT_EQ (Cubic->HyStartState , HYSTART_DONE); // Still DONE
1569- }
1570-
15711447//
15721448// Test: HyStart++ Recovery Exit with State Persistence
15731449// Scenario: When exiting recovery (IsInRecovery: TRUE → FALSE), the HyStart
@@ -2243,6 +2119,8 @@ TEST_F(CubicTest, Recovery_DoubleLossNoDoubleReduction)
22432119 ASSERT_EQ (Cubic->BytesInFlight , BytesInFlightAfterFirstLoss - SecondLostBytes);
22442120}
22452121
2122+ //
2123+ // Test: Pacing LastSendAllowance Carryover
22462124// Scenario: When pacing is enabled and GetSendAllowance is called multiple times
22472125// without sending data, the unused allowance (LastSendAllowance) carries over
22482126// and accumulates into the next call's result.
0 commit comments