net.luminis.quic.recovery.RttEstimator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of kwik Show documentation
Show all versions of kwik Show documentation
A QUIC implementation in Java
/*
* Copyright © 2019, 2020, 2021, 2022, 2023 Peter Doornbosch
*
* This file is part of Kwik, an implementation of the QUIC protocol in Java.
*
* Kwik is free software: you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* Kwik 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 Lesser General Public License for
* more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*/
package net.luminis.quic.recovery;
import net.luminis.quic.frame.AckFrame;
import net.luminis.quic.log.Logger;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
public class RttEstimator {
private static final int NOT_SET = -1;
private final Logger log;
// All intervals are in milliseconds (1/1000 second)
private volatile int initialRtt;
private volatile int minRtt = Integer.MAX_VALUE;
private volatile int smoothedRtt = NOT_SET;
private volatile int rttVar = NOT_SET;
private volatile int latestRtt;
private volatile int maxAckDelay;
public RttEstimator(Logger log) {
this.log = log;
// https://tools.ietf.org/html/draft-ietf-quic-recovery-20#section-6.2
// "If no previous RTT is available, or if the network
// changes, the initial RTT SHOULD be set to 500ms"
initialRtt = 500;
// https://tools.ietf.org/html/draft-ietf-quic-transport-30#section-8.2
// "If this value is absent, a default of 25 milliseconds is assumed."
maxAckDelay = 25;
}
public RttEstimator(Logger log, int initialRtt) {
this.log = log;
this.initialRtt = initialRtt;
}
public void addSample(Instant timeReceived, Instant timeSent, int ackDelay) {
if (timeReceived.isBefore(timeSent)) {
// This sometimes happens in the Interop runner; reconsider solution after new sender is implemented.
log.error("Receiving negative rtt estimate: sent=" + timeSent + ", received=" + timeReceived);
return;
}
if (ackDelay > maxAckDelay) {
ackDelay = maxAckDelay;
}
int previousSmoothed = smoothedRtt;
int rttSample = (int) Duration.between(timeSent, timeReceived).toMillis();
if (rttSample < minRtt)
minRtt = rttSample;
// Adjust for ack delay if it's plausible. Because times are truncated at millisecond precision,
// consider rtt equal to min as plausible.
if (rttSample >= minRtt + ackDelay) {
rttSample -= ackDelay;
}
latestRtt = rttSample;
if (smoothedRtt == NOT_SET) {
// First time
smoothedRtt = rttSample;
rttVar = rttSample / 2;
}
else {
int currentRttVar = Math.abs(smoothedRtt - rttSample);
rttVar = (3 * rttVar + currentRttVar + 2) / 4; // Add 2 to round to nearest integer
smoothedRtt = (7 * smoothedRtt + rttSample + 4) / 8; // Add 4 to round to nearest integer
}
log.debug("RTT: " + previousSmoothed + " + " + rttSample + " -> " + smoothedRtt);
}
public int getSmoothedRtt() {
if (smoothedRtt == NOT_SET) {
return initialRtt;
}
else {
return smoothedRtt;
}
}
public int getRttVar() {
// Rtt-var is only used for computing PTO.
// https://tools.ietf.org/html/draft-ietf-quic-recovery-23#section-5.3
// "The initial probe timeout for a new connection or new path SHOULD be set to twice the initial RTT"
// https://tools.ietf.org/html/draft-ietf-quic-recovery-23#section-5.2.1
// "PTO = smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay"
// Hence, using an initial rtt-var of initial-rtt / 4, will result in an initial PTO of twice the initial RTT.
// After the first packet is received, the rttVar will be computed from the real RTT sample.
if (rttVar == NOT_SET) {
return initialRtt / 4;
}
else {
return rttVar;
}
}
public void ackReceived(AckFrame ack, Instant timeReceived, List newlyAcked) {
// https://tools.ietf.org/html/draft-ietf-quic-recovery-33#section-5.1
// "An endpoint generates an RTT sample on receiving an ACK frame that meets the following two conditions:
// * the largest acknowledged packet number is newly acknowledged, and
// * at least one of the newly acknowledged packets was ack-eliciting."
Optional largestAcked = newlyAcked.stream()
.filter(s -> s.packet().getPacketNumber() == ack.getLargestAcknowledged()) // Possible optimization: isn't newlyAcked sorted, so only checking first will do?
.findFirst();
if (largestAcked.isPresent()) {
if (newlyAcked.stream().anyMatch(s -> s.packet().isAckEliciting())) {
addSample(timeReceived, largestAcked.get().timeSent(), ack.getAckDelay());
}
}
}
public int getLatestRtt() {
return latestRtt;
}
public void setMaxAckDelay(int maxAckDelay) {
this.maxAckDelay = maxAckDelay;
}
}