github.com/metacubex/quic-go@v0.44.1-0.20240520163451-20b689a59136/internal/congestion/cubic.go (about)

     1  package congestion
     2  
     3  import (
     4  	"math"
     5  	"time"
     6  
     7  	"github.com/metacubex/quic-go/internal/protocol"
     8  	"github.com/metacubex/quic-go/internal/utils"
     9  )
    10  
    11  // This cubic implementation is based on the one found in Chromiums's QUIC
    12  // implementation, in the files net/quic/congestion_control/cubic.{hh,cc}.
    13  
    14  // Constants based on TCP defaults.
    15  // The following constants are in 2^10 fractions of a second instead of ms to
    16  // allow a 10 shift right to divide.
    17  
    18  // 1024*1024^3 (first 1024 is from 0.100^3)
    19  // where 0.100 is 100 ms which is the scaling round trip time.
    20  const (
    21  	cubeScale                 = 40
    22  	cubeCongestionWindowScale = 410
    23  	cubeFactor                = 1 << cubeScale / cubeCongestionWindowScale / maxDatagramSize
    24  	// TODO: when re-enabling cubic, make sure to use the actual packet size here
    25  	maxDatagramSize = protocol.ByteCount(protocol.InitialPacketSize)
    26  )
    27  
    28  const defaultNumConnections = 1
    29  
    30  // Default Cubic backoff factor
    31  const beta float32 = 0.7
    32  
    33  // Additional backoff factor when loss occurs in the concave part of the Cubic
    34  // curve. This additional backoff factor is expected to give up bandwidth to
    35  // new concurrent flows and speed up convergence.
    36  const betaLastMax float32 = 0.85
    37  
    38  // Cubic implements the cubic algorithm from TCP
    39  type Cubic struct {
    40  	clock Clock
    41  
    42  	// Number of connections to simulate.
    43  	numConnections int
    44  
    45  	// Time when this cycle started, after last loss event.
    46  	epoch time.Time
    47  
    48  	// Max congestion window used just before last loss event.
    49  	// Note: to improve fairness to other streams an additional back off is
    50  	// applied to this value if the new value is below our latest value.
    51  	lastMaxCongestionWindow protocol.ByteCount
    52  
    53  	// Number of acked bytes since the cycle started (epoch).
    54  	ackedBytesCount protocol.ByteCount
    55  
    56  	// TCP Reno equivalent congestion window in packets.
    57  	estimatedTCPcongestionWindow protocol.ByteCount
    58  
    59  	// Origin point of cubic function.
    60  	originPointCongestionWindow protocol.ByteCount
    61  
    62  	// Time to origin point of cubic function in 2^10 fractions of a second.
    63  	timeToOriginPoint uint32
    64  
    65  	// Last congestion window in packets computed by cubic function.
    66  	lastTargetCongestionWindow protocol.ByteCount
    67  }
    68  
    69  // NewCubic returns a new Cubic instance
    70  func NewCubic(clock Clock) *Cubic {
    71  	c := &Cubic{
    72  		clock:          clock,
    73  		numConnections: defaultNumConnections,
    74  	}
    75  	c.Reset()
    76  	return c
    77  }
    78  
    79  // Reset is called after a timeout to reset the cubic state
    80  func (c *Cubic) Reset() {
    81  	c.epoch = time.Time{}
    82  	c.lastMaxCongestionWindow = 0
    83  	c.ackedBytesCount = 0
    84  	c.estimatedTCPcongestionWindow = 0
    85  	c.originPointCongestionWindow = 0
    86  	c.timeToOriginPoint = 0
    87  	c.lastTargetCongestionWindow = 0
    88  }
    89  
    90  func (c *Cubic) alpha() float32 {
    91  	// TCPFriendly alpha is described in Section 3.3 of the CUBIC paper. Note that
    92  	// beta here is a cwnd multiplier, and is equal to 1-beta from the paper.
    93  	// We derive the equivalent alpha for an N-connection emulation as:
    94  	b := c.beta()
    95  	return 3 * float32(c.numConnections) * float32(c.numConnections) * (1 - b) / (1 + b)
    96  }
    97  
    98  func (c *Cubic) beta() float32 {
    99  	// kNConnectionBeta is the backoff factor after loss for our N-connection
   100  	// emulation, which emulates the effective backoff of an ensemble of N
   101  	// TCP-Reno connections on a single loss event. The effective multiplier is
   102  	// computed as:
   103  	return (float32(c.numConnections) - 1 + beta) / float32(c.numConnections)
   104  }
   105  
   106  func (c *Cubic) betaLastMax() float32 {
   107  	// betaLastMax is the additional backoff factor after loss for our
   108  	// N-connection emulation, which emulates the additional backoff of
   109  	// an ensemble of N TCP-Reno connections on a single loss event. The
   110  	// effective multiplier is computed as:
   111  	return (float32(c.numConnections) - 1 + betaLastMax) / float32(c.numConnections)
   112  }
   113  
   114  // OnApplicationLimited is called on ack arrival when sender is unable to use
   115  // the available congestion window. Resets Cubic state during quiescence.
   116  func (c *Cubic) OnApplicationLimited() {
   117  	// When sender is not using the available congestion window, the window does
   118  	// not grow. But to be RTT-independent, Cubic assumes that the sender has been
   119  	// using the entire window during the time since the beginning of the current
   120  	// "epoch" (the end of the last loss recovery period). Since
   121  	// application-limited periods break this assumption, we reset the epoch when
   122  	// in such a period. This reset effectively freezes congestion window growth
   123  	// through application-limited periods and allows Cubic growth to continue
   124  	// when the entire window is being used.
   125  	c.epoch = time.Time{}
   126  }
   127  
   128  // CongestionWindowAfterPacketLoss computes a new congestion window to use after
   129  // a loss event. Returns the new congestion window in packets. The new
   130  // congestion window is a multiplicative decrease of our current window.
   131  func (c *Cubic) CongestionWindowAfterPacketLoss(currentCongestionWindow protocol.ByteCount) protocol.ByteCount {
   132  	if currentCongestionWindow+maxDatagramSize < c.lastMaxCongestionWindow {
   133  		// We never reached the old max, so assume we are competing with another
   134  		// flow. Use our extra back off factor to allow the other flow to go up.
   135  		c.lastMaxCongestionWindow = protocol.ByteCount(c.betaLastMax() * float32(currentCongestionWindow))
   136  	} else {
   137  		c.lastMaxCongestionWindow = currentCongestionWindow
   138  	}
   139  	c.epoch = time.Time{} // Reset time.
   140  	return protocol.ByteCount(float32(currentCongestionWindow) * c.beta())
   141  }
   142  
   143  // CongestionWindowAfterAck computes a new congestion window to use after a received ACK.
   144  // Returns the new congestion window in packets. The new congestion window
   145  // follows a cubic function that depends on the time passed since last
   146  // packet loss.
   147  func (c *Cubic) CongestionWindowAfterAck(
   148  	ackedBytes protocol.ByteCount,
   149  	currentCongestionWindow protocol.ByteCount,
   150  	delayMin time.Duration,
   151  	eventTime time.Time,
   152  ) protocol.ByteCount {
   153  	c.ackedBytesCount += ackedBytes
   154  
   155  	if c.epoch.IsZero() {
   156  		// First ACK after a loss event.
   157  		c.epoch = eventTime            // Start of epoch.
   158  		c.ackedBytesCount = ackedBytes // Reset count.
   159  		// Reset estimated_tcp_congestion_window_ to be in sync with cubic.
   160  		c.estimatedTCPcongestionWindow = currentCongestionWindow
   161  		if c.lastMaxCongestionWindow <= currentCongestionWindow {
   162  			c.timeToOriginPoint = 0
   163  			c.originPointCongestionWindow = currentCongestionWindow
   164  		} else {
   165  			c.timeToOriginPoint = uint32(math.Cbrt(float64(cubeFactor * (c.lastMaxCongestionWindow - currentCongestionWindow))))
   166  			c.originPointCongestionWindow = c.lastMaxCongestionWindow
   167  		}
   168  	}
   169  
   170  	// Change the time unit from microseconds to 2^10 fractions per second. Take
   171  	// the round trip time in account. This is done to allow us to use shift as a
   172  	// divide operator.
   173  	elapsedTime := int64(eventTime.Add(delayMin).Sub(c.epoch)/time.Microsecond) << 10 / (1000 * 1000)
   174  
   175  	// Right-shifts of negative, signed numbers have implementation-dependent
   176  	// behavior, so force the offset to be positive, as is done in the kernel.
   177  	offset := int64(c.timeToOriginPoint) - elapsedTime
   178  	if offset < 0 {
   179  		offset = -offset
   180  	}
   181  
   182  	deltaCongestionWindow := protocol.ByteCount(cubeCongestionWindowScale*offset*offset*offset) * maxDatagramSize >> cubeScale
   183  	var targetCongestionWindow protocol.ByteCount
   184  	if elapsedTime > int64(c.timeToOriginPoint) {
   185  		targetCongestionWindow = c.originPointCongestionWindow + deltaCongestionWindow
   186  	} else {
   187  		targetCongestionWindow = c.originPointCongestionWindow - deltaCongestionWindow
   188  	}
   189  	// Limit the CWND increase to half the acked bytes.
   190  	targetCongestionWindow = utils.Min(targetCongestionWindow, currentCongestionWindow+c.ackedBytesCount/2)
   191  
   192  	// Increase the window by approximately Alpha * 1 MSS of bytes every
   193  	// time we ack an estimated tcp window of bytes.  For small
   194  	// congestion windows (less than 25), the formula below will
   195  	// increase slightly slower than linearly per estimated tcp window
   196  	// of bytes.
   197  	c.estimatedTCPcongestionWindow += protocol.ByteCount(float32(c.ackedBytesCount) * c.alpha() * float32(maxDatagramSize) / float32(c.estimatedTCPcongestionWindow))
   198  	c.ackedBytesCount = 0
   199  
   200  	// We have a new cubic congestion window.
   201  	c.lastTargetCongestionWindow = targetCongestionWindow
   202  
   203  	// Compute target congestion_window based on cubic target and estimated TCP
   204  	// congestion_window, use highest (fastest).
   205  	if targetCongestionWindow < c.estimatedTCPcongestionWindow {
   206  		targetCongestionWindow = c.estimatedTCPcongestionWindow
   207  	}
   208  	return targetCongestionWindow
   209  }
   210  
   211  // SetNumConnections sets the number of emulated connections
   212  func (c *Cubic) SetNumConnections(n int) {
   213  	c.numConnections = n
   214  }