8370024: HttpClient: QUIC congestion controller doesn't implement pacing

Reviewed-by: dfuchs
This commit is contained in:
Daniel Jeliński 2025-11-12 12:32:05 +00:00
parent 400a83da89
commit 1f1f7bb448
7 changed files with 673 additions and 16 deletions

View File

@ -97,6 +97,7 @@ public sealed class PacketSpaceManager implements PacketSpace
private final QuicCongestionController congestionController;
private volatile boolean blockedByCC;
private volatile boolean blockedByPacer;
// packet threshold for loss detection; RFC 9002 suggests 3
private static final long kPacketThreshold = 3;
// Multiplier for persistent congestion; RFC 9002 suggests 3
@ -386,6 +387,7 @@ public sealed class PacketSpaceManager implements PacketSpace
// Handle is called from within the executor
var nextDeadline = this.nextDeadline;
Deadline now = now();
congestionController.updatePacer(now);
do {
transmitNow = false;
var closed = !isOpenForTransmission();
@ -404,6 +406,7 @@ public sealed class PacketSpaceManager implements PacketSpace
boolean needBackoff = isPTO(now);
int packetsSent = 0;
boolean cwndAvailable;
long startTime = System.nanoTime();
while ((cwndAvailable = congestionController.canSendPacket()) ||
(needBackoff && packetsSent < 2)) { // if PTO, try to send 2 packets
if (!isOpenForTransmission()) {
@ -442,6 +445,7 @@ public sealed class PacketSpaceManager implements PacketSpace
+ qkue.getMessage());
}
if (!sentNew) {
congestionController.appLimited();
break;
} else {
if (needBackoff && packetsSent == 0 && Log.quicRetransmit()) {
@ -451,12 +455,19 @@ public sealed class PacketSpaceManager implements PacketSpace
}
packetsSent++;
}
blockedByCC = !cwndAvailable;
if (packetsSent != 0 && Log.quicCC()) {
Log.logQuic("%s OUT: sent: %s packets in %s ns, cwnd limited: %s, pacer limited: %s".formatted(
packetEmitter.logTag(), packetsSent, System.nanoTime() - startTime,
congestionController.isCwndLimited(), congestionController.isPacerLimited()));
}
blockedByCC = !cwndAvailable && congestionController.isCwndLimited();
blockedByPacer = !cwndAvailable && congestionController.isPacerLimited();
if (!cwndAvailable && isOpenForTransmission()) {
if (debug.on()) debug.log("handle: blocked by CC");
// CC might be available already
if (congestionController.canSendPacket()) {
if (debug.on()) debug.log("handle: unblocked immediately");
blockedByCC = blockedByPacer = false;
transmitNow = true;
}
}
@ -1389,6 +1400,15 @@ public sealed class PacketSpaceManager implements PacketSpace
Deadline ackDeadline = (ack == null || ack.sent() != null)
? Deadline.MAX // if the ack frame has already been sent, getNextAck() returns null
: ack.deadline();
if (blockedByPacer) {
Deadline pacerDeadline = congestionController.pacerDeadline();
if (verbose && Log.quicTimer()) {
Log.logQuic(String.format("%s: [%s] pacer deadline: %s, ackDeadline: %s, deadline in %s",
packetEmitter.logTag(), packetNumberSpace, pacerDeadline, ackDeadline,
Utils.debugDeadline(now(), min(ackDeadline, pacerDeadline))));
}
return min(ackDeadline, pacerDeadline);
}
Deadline lossDeadline = getLossTimer();
// TODO: consider removing the debug traces in this method when integrating
// if both loss deadline and PTO timer are set, loss deadline is always earlier

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -72,4 +72,49 @@ public interface QuicCongestionController {
*/
void packetDiscarded(Collection<QuicPacket> discardedPackets);
/**
* {@return the current size of the congestion window in bytes}
*/
long congestionWindow();
/**
* {@return the initial window size in bytes}
*/
long initialWindow();
/**
* {@return maximum datagram size}
*/
long maxDatagramSize();
/**
* {@return true if the connection is in slow start phase}
*/
boolean isSlowStart();
/**
* Update the pacer with the current time
* @param now the current time
*/
void updatePacer(Deadline now);
/**
* {@return true if sending is blocked by pacer}
*/
boolean isPacerLimited();
/**
* {@return true if sending is blocked by congestion window}
*/
boolean isCwndLimited();
/**
* {@return deadline when pacer will unblock sending}
*/
Deadline pacerDeadline();
/**
* Notify the congestion controller that sending is app-limited
*/
void appLimited();
}

View File

@ -334,7 +334,7 @@ public class QuicConnectionImpl extends QuicConnection implements QuicPacketRece
this.connectionId = this.endpoint.idFactory().newConnectionId();
this.logTag = logTagFormat.formatted(labelId);
this.dbgTag = dbgTag(quicInstance, logTag);
this.congestionController = new QuicRenoCongestionController(dbgTag);
this.congestionController = new QuicRenoCongestionController(dbgTag, rttEstimator);
this.originalVersion = this.quicVersion = firstFlightVersion == null
? QuicVersion.firstFlightVersion(quicInstance.getAvailableVersions())
: firstFlightVersion;

View File

@ -0,0 +1,191 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.internal.net.http.quic;
import jdk.internal.net.http.common.Deadline;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.Utils;
import jdk.internal.util.OperatingSystem;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* Implementation of pacing.
*
* When the connection is sending at a rate lower than permitted
* by the congestion controller, pacer is responsible for spreading out
* the outgoing packets across the entire RTT.
*
* Technically the pacer provides two functions:
* - computes the number of packets that can be sent now
* - computes the time when another packet can be sent
*
* When a new flow starts, or when the flow is not pacer-limited,
* the pacer limits the window to:
* max(INITIAL_WINDOW, pacingRate / timerFreq)
* timerFreq is the best timer resolution we can get from the selector.
* pacingRate is N * congestionWindow / smoothedRTT
* where N = 2 when in slow start, N = 1.25 otherwise.
*
* After that, the window refills at pacingRate, up to two timer periods or 4 packets,
* whichever is higher.
*
* The time when another packet can be sent is computed
* as the time when the window will allow at least 2 packets.
*
* All methods are externally synchronized in congestion controller.
*
* Ideas taken from:
* https://www.rfc-editor.org/rfc/rfc9002.html#name-pacing
* https://www.ietf.org/archive/id/draft-welzl-iccrg-pacing-03.html
*/
public class QuicPacer {
// usually 64 Hz on Windows, 1000 on Linux
private static final long DEFAULT_TIMER_FREQ_HZ = OperatingSystem.isWindows() ? 64 : 1000;
private static final long TIMER_FREQ_HZ = Math.clamp(
Utils.getLongProperty("jdk.httpclient.quic.timerFrequency", DEFAULT_TIMER_FREQ_HZ),
1, 1000);
private final QuicRttEstimator rttEstimator;
private final QuicCongestionController congestionController;
private boolean appLimited;
private long quota;
private Deadline lastUpdate;
/**
* Create a QUIC pacer for the given RTT estimator and congestion controller
*
* @param rttEstimator the RTT estimator
* @param congestionController the congestion controller
*/
public QuicPacer(QuicRttEstimator rttEstimator,
QuicCongestionController congestionController) {
this.rttEstimator = rttEstimator;
this.congestionController = congestionController;
this.appLimited = true;
}
/**
* called to indicate that the flow is app-limited.
* Alters the behavior of the following updateQuota call.
*/
public void appLimited() {
appLimited = true;
}
/**
* {@return true if pacer quota not hit yet, false otherwise}
*/
public boolean canSend() {
return quota >= congestionController.maxDatagramSize();
}
/**
* Update quota based on time since the last call to this method
* and whether appLimited() was called or not.
*
* @param now current time
*/
public void updateQuota(Deadline now) {
if (lastUpdate != null && !now.isAfter(lastUpdate)) {
// might happen when transmission tasks from different packet spaces
// race to update quota. Keep the most recent update only.
return;
}
long rttMicros = rttEstimator.state().smoothedRttMicros();
long cwnd = congestionController.congestionWindow();
if (rttMicros * TIMER_FREQ_HZ < TimeUnit.SECONDS.toMicros(2)) {
// RTT less than two timer periods; don't pace
quota = 2 * cwnd;
lastUpdate = now;
return;
}
long pacingRate = cwnd * (congestionController.isSlowStart() ? 2_000_000 : 1_250_000) / rttMicros; // bytes per second
long initialWindow = congestionController.initialWindow();
long onePeriodWindow = pacingRate / TIMER_FREQ_HZ;
long maxQuota;
if (appLimited) {
maxQuota = Math.max(initialWindow, onePeriodWindow);
} else {
maxQuota = Math.max(2 * onePeriodWindow, 4 * congestionController.maxDatagramSize());
}
if (lastUpdate == null) {
quota = Math.max(initialWindow, maxQuota);
} else {
long nanosSinceUpdate = Deadline.between(lastUpdate, now).toNanos();
if (nanosSinceUpdate >= TimeUnit.MICROSECONDS.toNanos(rttMicros)) {
// don't bother computing the increment, it might overflow and will be capped to maxQuota anyway
quota = maxQuota;
if (Log.quicCC()) {
Log.logQuic("pacer cwnd: %s, rtt %s us, duration %s ns, quota: %s".formatted(
cwnd, rttMicros, nanosSinceUpdate, quota));
}
} else {
long quotaIncrement = pacingRate * nanosSinceUpdate / 1_000_000_000;
quota += quotaIncrement;
quota = Math.min(quota, maxQuota);
if (Log.quicCC()) {
Log.logQuic("pacer cwnd: %s, rtt %s us, duration %s ns, increment %s, quota %s".formatted(
cwnd, rttMicros, nanosSinceUpdate, quotaIncrement, quota));
}
}
}
lastUpdate = now;
appLimited = false;
}
/**
* {@return the deadline when quota will increase to two packets}
*/
public Deadline twoPacketDeadline() {
long datagramSize = congestionController.maxDatagramSize();
long quotaNeeded = datagramSize * 2 - quota;
if (quotaNeeded <= 0) {
assert canSend();
return lastUpdate;
}
// Window increases at a rate of rtt / cwnd / N
long rttMicros = rttEstimator.state().smoothedRttMicros();
long cwnd = congestionController.congestionWindow();
return lastUpdate.plus(rttMicros
* (congestionController.isSlowStart() ? 500 : 800) /* 1000/N */
* quotaNeeded / cwnd, ChronoUnit.NANOS);
}
/**
* called to indicate that a packet was sent
*
* @param packetBytes packet size in bytes
*/
public void packetSent(int packetBytes) {
quota -= packetBytes;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -64,9 +64,12 @@ class QuicRenoCongestionController implements QuicCongestionController {
private Deadline congestionRecoveryStartTime;
private long ssThresh = Long.MAX_VALUE;
public QuicRenoCongestionController(String dbgTag) {
private final QuicPacer pacer;
public QuicRenoCongestionController(String dbgTag, QuicRttEstimator rttEstimator) {
this.dbgTag = dbgTag;
this.timeSource = TimeSource.source();
this.pacer = new QuicPacer(rttEstimator, this);
}
private boolean inCongestionRecovery(Deadline sentTime) {
@ -83,7 +86,7 @@ class QuicRenoCongestionController implements QuicCongestionController {
congestionWindow = Math.max(minimumWindow, ssThresh);
maxBytesInFlight = 0;
if (Log.quicCC()) {
Log.logQuic(dbgTag+ " Congestion: ssThresh: " + ssThresh +
Log.logQuic(dbgTag + " Congestion: ssThresh: " + ssThresh +
", in flight: " + bytesInFlight +
", cwnd:" + congestionWindow);
}
@ -103,8 +106,10 @@ class QuicRenoCongestionController implements QuicCongestionController {
if (bytesInFlight >= MAX_BYTES_IN_FLIGHT) {
return false;
}
var canSend = congestionWindow - bytesInFlight >= maxDatagramSize;
return canSend;
if (isCwndLimited() || isPacerLimited()) {
return false;
}
return true;
} finally {
lock.unlock();
}
@ -132,6 +137,7 @@ class QuicRenoCongestionController implements QuicCongestionController {
if (bytesInFlight > maxBytesInFlight) {
maxBytesInFlight = bytesInFlight;
}
pacer.packetSent(packetBytes);
} finally {
lock.unlock();
}
@ -147,8 +153,8 @@ class QuicRenoCongestionController implements QuicCongestionController {
// Here we limit cwnd growth based on the maximum bytes in flight
// observed since the last congestion event
if (inCongestionRecovery(sentTime)) {
if (Log.quicCC()) {
Log.logQuic(dbgTag+ " Acked, in recovery: bytes: " + packetBytes +
if (Log.quicCC() && Log.trace()) {
Log.logQuic(dbgTag + " Acked, in recovery: bytes: " + packetBytes +
", in flight: " + bytesInFlight);
}
return;
@ -165,9 +171,9 @@ class QuicRenoCongestionController implements QuicCongestionController {
congestionWindow += Math.max((long) maxDatagramSize * packetBytes / congestionWindow, 1L);
}
}
if (Log.quicCC()) {
if (Log.quicCC() && Log.trace()) {
if (isAppLimited) {
Log.logQuic(dbgTag+ " Acked, not blocked: bytes: " + packetBytes +
Log.logQuic(dbgTag + " Acked, not blocked: bytes: " + packetBytes +
", in flight: " + bytesInFlight);
} else {
Log.logQuic(dbgTag + " Acked, increased: bytes: " + packetBytes +
@ -194,7 +200,7 @@ class QuicRenoCongestionController implements QuicCongestionController {
congestionWindow = minimumWindow;
congestionRecoveryStartTime = null;
if (Log.quicCC()) {
Log.logQuic(dbgTag+ " Persistent congestion: ssThresh: " + ssThresh +
Log.logQuic(dbgTag + " Persistent congestion: ssThresh: " + ssThresh +
", in flight: " + bytesInFlight +
", cwnd:" + congestionWindow);
}
@ -217,4 +223,94 @@ class QuicRenoCongestionController implements QuicCongestionController {
lock.unlock();
}
}
@Override
public long congestionWindow() {
lock.lock();
try {
return congestionWindow;
} finally {
lock.unlock();
}
}
@Override
public long initialWindow() {
lock.lock();
try {
return Math.max(14720, 2 * maxDatagramSize);
} finally {
lock.unlock();
}
}
@Override
public long maxDatagramSize() {
lock.lock();
try {
return maxDatagramSize;
} finally {
lock.unlock();
}
}
@Override
public boolean isSlowStart() {
lock.lock();
try {
return congestionWindow < ssThresh;
} finally {
lock.unlock();
}
}
@Override
public void updatePacer(Deadline now) {
lock.lock();
try {
pacer.updateQuota(now);
} finally {
lock.unlock();
}
}
@Override
public boolean isPacerLimited() {
lock.lock();
try {
return !pacer.canSend();
} finally {
lock.unlock();
}
}
@Override
public boolean isCwndLimited() {
lock.lock();
try {
return congestionWindow - bytesInFlight < maxDatagramSize;
} finally {
lock.unlock();
}
}
@Override
public Deadline pacerDeadline() {
lock.lock();
try {
return pacer.twoPacketDeadline();
} finally {
lock.unlock();
}
}
@Override
public void appLimited() {
lock.lock();
try {
pacer.appLimited();
} finally {
lock.unlock();
}
}
}

View File

@ -0,0 +1,273 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import jdk.internal.net.http.common.Deadline;
import jdk.internal.net.http.quic.QuicCongestionController;
import jdk.internal.net.http.quic.QuicPacer;
import jdk.internal.net.http.quic.QuicRttEstimator;
import jdk.internal.net.http.quic.packets.QuicPacket;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
/*
* @test
* @run testng/othervm -Djdk.httpclient.quic.timerFrequency=1000 PacerTest
*/
public class PacerTest {
private static class TestCongestionController implements QuicCongestionController {
private long cwnd;
private long iw;
private long maxDatagramSize;
private boolean isSlowStart;
private TestCongestionController(long cwnd, long iw, long maxDatagramSize, boolean isSlowStart) {
this.cwnd = cwnd;
this.iw = iw;
this.maxDatagramSize = maxDatagramSize;
this.isSlowStart = isSlowStart;
}
@Override
public boolean canSendPacket() {
throw new AssertionError("Should not come here");
}
@Override
public void updateMaxDatagramSize(int newSize) {
throw new AssertionError("Should not come here");
}
@Override
public void packetSent(int packetBytes) {
throw new AssertionError("Should not come here");
}
@Override
public void packetAcked(int packetBytes, Deadline sentTime) {
throw new AssertionError("Should not come here");
}
@Override
public void packetLost(Collection<QuicPacket> lostPackets, Deadline sentTime, boolean persistent) {
throw new AssertionError("Should not come here");
}
@Override
public void packetDiscarded(Collection<QuicPacket> discardedPackets) {
throw new AssertionError("Should not come here");
}
@Override
public long congestionWindow() {
return cwnd;
}
@Override
public long initialWindow() {
return iw;
}
@Override
public long maxDatagramSize() {
return maxDatagramSize;
}
@Override
public boolean isSlowStart() {
return isSlowStart;
}
@Override
public void updatePacer(Deadline now) {
throw new AssertionError("Should not come here");
}
@Override
public boolean isPacerLimited() {
throw new AssertionError("Should not come here");
}
@Override
public boolean isCwndLimited() {
throw new AssertionError("Should not come here");
}
@Override
public Deadline pacerDeadline() {
throw new AssertionError("Should not come here");
}
@Override
public void appLimited() {
throw new AssertionError("Should not come here");
}
}
public record TestCase(int maxDatagramSize, int packetsInIW, int packetsInCwnd, int millisInRtt,
int initialPermit, int periodicPermit, boolean slowStart) {
}
@DataProvider
public Object[][] pacerFirstFlight() {
return List.of(
// Should permit initial window before blocking
new TestCase(1200, 10, 32, 16, 10, 4, true),
// Should permit 2*cwnd/rtt packets before blocking
new TestCase(1200, 10, 128, 16, 16, 16, true),
// Should permit 1.25*cwnd/rtt packets before blocking
new TestCase(1200, 10, 256, 16, 20, 20, false)
).stream().map(Stream::of)
.map(Stream::toArray)
.toArray(Object[][]::new);
}
@Test(dataProvider = "pacerFirstFlight")
public void testBasicPacing(TestCase test) {
int maxDatagramSize = test.maxDatagramSize;
int packetsInIW = test.packetsInIW;
int packetsInCwnd = test.packetsInCwnd;
int millisInRtt = test.millisInRtt;
int permit = test.initialPermit;
QuicCongestionController cc = new TestCongestionController(packetsInCwnd * maxDatagramSize,
maxDatagramSize * packetsInIW, maxDatagramSize, test.slowStart);
QuicRttEstimator rtt = new QuicRttEstimator();
rtt.consumeRttSample(1000 * millisInRtt, 0, Deadline.MIN);
QuicPacer pacer = new QuicPacer(rtt, cc);
pacer.updateQuota(Deadline.MIN);
for (int i = 0; i < permit; i++) {
assertTrue(pacer.canSend(), "Pacer blocked after " + i + " packets");
pacer.packetSent(maxDatagramSize);
}
assertFalse(pacer.canSend(), "Pacer didn't block");
Deadline next = pacer.twoPacketDeadline();
pacer.updateQuota(next);
for (int i = 0; i < 2; i++) {
assertTrue(pacer.canSend(), "Two packet deadline: pacer blocked after " + i + " packets");
pacer.packetSent(maxDatagramSize);
}
assertFalse(pacer.canSend(), "Two packet deadline: pacer didn't block");
next = next.plus(1, ChronoUnit.MILLIS);
pacer.updateQuota(next);
for (int i = 0; i < test.periodicPermit; i++) {
assertTrue(pacer.canSend(), "One millisecond: pacer blocked after " + i + " packets");
pacer.packetSent(maxDatagramSize);
}
assertFalse(pacer.canSend(), "One millisecond: pacer didn't block");
next = next.plus(3, ChronoUnit.MILLIS);
pacer.updateQuota(next);
// Quota capped at two millisecond equivalent
for (int i = 0; i < 2 * test.periodicPermit; i++) {
assertTrue(pacer.canSend(), "Three milliseconds: pacer blocked after " + i + " packets");
pacer.packetSent(maxDatagramSize);
}
assertFalse(pacer.canSend(), "Three milliseconds: pacer didn't block");
next = next.plus(3, ChronoUnit.MILLIS);
pacer.appLimited();
pacer.updateQuota(next);
// App-limited: quota capped at initialPermit
for (int i = 0; i < test.initialPermit; i++) {
assertTrue(pacer.canSend(), "App limited: pacer blocked after " + i + " packets");
pacer.packetSent(maxDatagramSize);
}
assertFalse(pacer.canSend(), "App limited: pacer didn't block");
}
@Test
public void testPacingShortRtt() {
int maxDatagramSize = 1200;
int packetsInIW = 10;
int packetsInCwnd = 32;
QuicCongestionController cc = new TestCongestionController(packetsInCwnd * maxDatagramSize,
maxDatagramSize * packetsInIW, maxDatagramSize, true);
QuicRttEstimator rtt = new QuicRttEstimator();
rtt.consumeRttSample(1000, 0, Deadline.MIN);
QuicPacer pacer = new QuicPacer(rtt, cc);
pacer.updateQuota(Deadline.MIN);
for (int i = 0; i < 2 * packetsInCwnd; i++) {
assertTrue(pacer.canSend(), "Pacer blocked after " + i + " packets");
pacer.packetSent(maxDatagramSize);
}
assertFalse(pacer.canSend(), "Pacer didn't block");
// when RTT is short, permit cwnd on every update
Deadline next = pacer.twoPacketDeadline();
pacer.updateQuota(next);
for (int i = 0; i < 2 * packetsInCwnd; i++) {
assertTrue(pacer.canSend(), "Two packet deadline: pacer blocked after " + i + " packets");
pacer.packetSent(maxDatagramSize);
}
assertFalse(pacer.canSend(), "Two packet deadline: pacer didn't block");
}
@Test
public void testPacingSmallCwnd() {
int maxDatagramSize = 1200;
int packetsInIW = 10;
int packetsInCwnd = 2;
int millisInRtt = 16;
QuicCongestionController cc = new TestCongestionController(packetsInCwnd * maxDatagramSize,
maxDatagramSize * packetsInIW, maxDatagramSize, true);
QuicRttEstimator rtt = new QuicRttEstimator();
rtt.consumeRttSample(1000 * millisInRtt, 0, Deadline.MIN);
QuicPacer pacer = new QuicPacer(rtt, cc);
// first quota update is capped to IW
pacer.updateQuota(Deadline.MIN);
// update quota again. This time it's capped to 4 packets
pacer.updateQuota(Deadline.MIN.plusNanos(1));
for (int i = 0; i < 4; i++) {
assertTrue(pacer.canSend(), "Pacer blocked after " + i + " packets");
pacer.packetSent(maxDatagramSize);
}
assertFalse(pacer.canSend(), "Pacer didn't block");
Deadline next = pacer.twoPacketDeadline();
pacer.updateQuota(next);
for (int i = 0; i < 2; i++) {
assertTrue(pacer.canSend(), "Two packet deadline: pacer blocked after " + i + " packets");
pacer.packetSent(maxDatagramSize);
}
assertFalse(pacer.canSend(), "Two packet deadline: pacer didn't block");
// pacing rate is 1 packet per 4 milliseconds
next = next.plus(4, ChronoUnit.MILLIS);
pacer.updateQuota(next);
assertTrue(pacer.canSend(), "Pacer blocked after 4 millis");
pacer.packetSent(maxDatagramSize);
assertFalse(pacer.canSend(), "Pacer permitted 2 packets after 4 millis");
next = next.plus(2, ChronoUnit.MILLIS);
pacer.updateQuota(next);
assertFalse(pacer.canSend(), "Pacer permitted a packet after 2 millis");
next = next.plus(2, ChronoUnit.MILLIS);
pacer.updateQuota(next);
assertTrue(pacer.canSend(), "Pacer blocked after 2x2 millis");
pacer.packetSent(maxDatagramSize);
assertFalse(pacer.canSend(), "Pacer permitted 2 packets after 2x2 millis");
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -45,10 +45,12 @@ import jdk.internal.net.http.common.Deadline;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.TestLoggerUtil;
import jdk.internal.net.http.common.TimeLine;
import jdk.internal.net.http.quic.CodingContext;
import jdk.internal.net.http.quic.PacketEmitter;
import jdk.internal.net.http.quic.PacketSpaceManager;
import jdk.internal.net.http.quic.PeerConnectionId;
import jdk.internal.net.http.quic.QuicCongestionController;
import jdk.internal.net.http.quic.QuicConnectionId;
import jdk.internal.net.http.quic.QuicRttEstimator;
import jdk.internal.net.http.quic.QuicTimerQueue;
import jdk.internal.net.http.quic.frames.AckFrame;
@ -61,8 +63,6 @@ import jdk.internal.net.http.quic.packets.InitialPacket;
import jdk.internal.net.http.quic.packets.PacketSpace;
import jdk.internal.net.http.quic.packets.QuicPacketDecoder;
import jdk.internal.net.http.quic.packets.QuicPacketEncoder;
import jdk.internal.net.http.quic.CodingContext;
import jdk.internal.net.http.quic.QuicConnectionId;
import jdk.internal.net.http.quic.packets.QuicPacket;
import jdk.internal.net.http.quic.packets.QuicPacket.PacketNumberSpace;
import jdk.internal.net.http.quic.packets.QuicPacket.PacketType;
@ -608,6 +608,38 @@ public class PacketSpaceManagerTest {
public void packetLost(Collection<QuicPacket> lostPackets, Deadline sentTime, boolean persistent) { }
@Override
public void packetDiscarded(Collection<QuicPacket> discardedPackets) { }
@Override
public long congestionWindow() {
return Integer.MAX_VALUE;
}
@Override
public long initialWindow() {
return Integer.MAX_VALUE;
}
@Override
public long maxDatagramSize() {
return 1200;
}
@Override
public boolean isSlowStart() {
return false;
}
@Override
public void updatePacer(Deadline now) { }
@Override
public boolean isPacerLimited() {
return false;
}
@Override
public boolean isCwndLimited() {
return false;
}
@Override
public Deadline pacerDeadline() {
return Deadline.MIN;
}
@Override
public void appLimited() { }
};
manager = new PacketSpaceManager(space, this, timeSource,
rttEstimator, congestionController, new DummyQuicTLSEngine(),