mirror of
https://github.com/openjdk/jdk.git
synced 2026-01-28 12:09:14 +00:00
8370024: HttpClient: QUIC congestion controller doesn't implement pacing
Reviewed-by: dfuchs
This commit is contained in:
parent
400a83da89
commit
1f1f7bb448
@ -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
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
273
test/jdk/java/net/httpclient/quic/PacerTest.java
Normal file
273
test/jdk/java/net/httpclient/quic/PacerTest.java
Normal 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");
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user