github.com/psiphon-Labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/common/quic/gquic-go/internal/congestion/cubic.go (about)

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