diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketSpaceManager.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketSpaceManager.java index 494af85447e..90031decdb4 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketSpaceManager.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/PacketSpaceManager.java @@ -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 diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicCongestionController.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicCongestionController.java index 4bfad2c5560..94dafaf16eb 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicCongestionController.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicCongestionController.java @@ -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 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(); } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java index 9a280c3a8a5..7ebe09e008e 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicConnectionImpl.java @@ -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; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicPacer.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicPacer.java new file mode 100644 index 00000000000..0ba7d78038b --- /dev/null +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicPacer.java @@ -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; + } +} diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRenoCongestionController.java b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRenoCongestionController.java index fde253740d1..ff51aafc131 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRenoCongestionController.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/quic/QuicRenoCongestionController.java @@ -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(); + } + } } diff --git a/test/jdk/java/net/httpclient/quic/PacerTest.java b/test/jdk/java/net/httpclient/quic/PacerTest.java new file mode 100644 index 00000000000..d54f4125a46 --- /dev/null +++ b/test/jdk/java/net/httpclient/quic/PacerTest.java @@ -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 lostPackets, Deadline sentTime, boolean persistent) { + throw new AssertionError("Should not come here"); + } + + @Override + public void packetDiscarded(Collection 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"); + } +} diff --git a/test/jdk/java/net/httpclient/quic/PacketSpaceManagerTest.java b/test/jdk/java/net/httpclient/quic/PacketSpaceManagerTest.java index 7cd2adfe7ab..40497ec13da 100644 --- a/test/jdk/java/net/httpclient/quic/PacketSpaceManagerTest.java +++ b/test/jdk/java/net/httpclient/quic/PacketSpaceManagerTest.java @@ -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 lostPackets, Deadline sentTime, boolean persistent) { } @Override public void packetDiscarded(Collection 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(),