@fails-components/webtransport
Version:
A component to add webtransport support (server and client) to node.js using libquiche
372 lines (330 loc) • 10.6 kB
JavaScript
import { logger } from '../utils.js'
/**
* @typedef {import('../types').FlowControlable} FlowControlable
*/
const pid = typeof process !== 'undefined' ? process.pid : 0
const log = logger(`webtransport:flowcontroller(${pid})`)
// Ported from libquiche, QuicFlowController so their license applies to the original in C++ and this javascript translation
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export class FlowController {
static kSessionFlowControlMultiplier = 1.5
/**
* @param {{tocontrol: FlowControlable
* sendWindowOffset: number,
* receiveWindowOffset: number,
* shouldAutoTuneReceiveWindow: boolean
* receiveWindowSizeLimit: number,
* sessionFlowController?: FlowController}} arg
*/
constructor({
tocontrol,
sendWindowOffset,
receiveWindowOffset,
receiveWindowSizeLimit,
shouldAutoTuneReceiveWindow,
sessionFlowController
}) {
this.tocontrol = tocontrol
this.bytesSent = 0n
this.sendWindowOffset = BigInt(sendWindowOffset)
this.bytesConsumed = 0n
this.highestReceivedByteOffset = 0n
this.receiveWindowOffset = BigInt(receiveWindowOffset)
this.receiveWindowSize = BigInt(receiveWindowOffset)
this.receiveWindowSizeLimit = BigInt(receiveWindowSizeLimit)
this.autoTuneReceiveWindow = shouldAutoTuneReceiveWindow
this.sessionFlowController = sessionFlowController
this.lastBlockedSendWindowOffset = 0n
this.prevWindowUpdateTime = undefined
log(
'Created flow controller ' +
', setting initial receive window offset to: ' +
this.receiveWindowOffset +
', max receive window to: ' +
this.receiveWindowSize +
', max receive window limit to: ' +
this.receiveWindowSizeLimit +
', setting send window offset to: ' +
this.sendWindowOffset
)
}
/**
* @param {Number} bytesConsumed
*/
addBytesConsumed(bytesConsumed) {
this.bytesConsumed += BigInt(bytesConsumed)
log(' consumed ' + bytesConsumed + ' bytes.')
this.maybeSendWindowUpdate()
}
/**
* @param {Number} increaseOffset
*/
updateHighestReceivedOffset(increaseOffset) {
// Only update if offset has increased.
/* if (newOffset <= this.highestReceivedByteOffset) {
return false
} */
log(
' highest byte offset increased from ' + this.highestReceivedByteOffset,
' to ',
this.highestReceivedByteOffset + BigInt(increaseOffset)
)
this.highestReceivedByteOffset += BigInt(increaseOffset)
return true
}
/**
* @param {Number} nbytesSent
*/
addBytesSent(nbytesSent) {
const bytesSent = BigInt(nbytesSent)
if (this.bytesSent + bytesSent > this.sendWindowOffset) {
log(
' Trying to send an extra ' +
bytesSent +
' bytes, when bytes_sent = ' +
this.bytesSent +
', and send_window_offset_ = ' +
this.sendWindowOffset
)
this.bytesSent = this.sendWindowOffset
// This is an error on our side, close the connection as soon as possible.
this.tocontrol.closeConnection({
code: 63, // QUIC_FLOW_CONTROL_SENT_TOO_MUCH_DATA,
reason:
this.sendWindowOffset -
(this.bytesSent + bytesSent) +
'bytes over send window offset'
})
return
}
this.bytesSent += bytesSent
log(' sent ' + bytesSent + ' bytes.')
}
flowControlViolation() {
if (this.highestReceivedByteOffset > this.receiveWindowOffset) {
log(
'Flow control violation on ' +
', receive window offset: ' +
this.receiveWindowOffset +
', highest received byte offset: ' +
this.highestReceivedByteOffset
)
return true
}
return false
}
maybeIncreaseMaxWindowSize() {
// Core of receive window auto tuning. This method should be called before a
// WINDOW_UPDATE frame is sent. Ideally, window updates should occur close to
// once per RTT. If a window update happens much faster than RTT, it implies
// that the flow control window is imposing a bottleneck. To prevent this,
// this method will increase the receive window size (subject to a reasonable
// upper bound). For simplicity this algorithm is deliberately asymmetric, in
// that it may increase window size but never decreases.
// Keep track of timing between successive window updates.
const now = Date.now()
const prev = this.prevWindowUpdateTime
this.prevWindowUpdateTime = now
if (!prev) {
log('first window update for ')
return
}
if (!this.autoTuneReceiveWindow) {
return
}
// TODO port need a replacement for this
// Get outbound RTT.
const rtt = this.tocontrol.smoothedRtt()
if (rtt === 0) {
log('rtt zero for ')
return
}
// Now we can compare timing of window updates with RTT.
const sinceLast = now - prev
const twoRtt = 2 * rtt
if (sinceLast >= twoRtt) {
// If interval between window updates is sufficiently large, there
// is no need to increase receive_window_size_.
return
}
const oldWindow = this.receiveWindowSize
this.increaseWindowSize()
if (this.receiveWindowSize > oldWindow) {
log(
'New max window increase for ' +
+' after ' +
sinceLast +
' us, and RTT is ' +
rtt +
'us. max wndw: ' +
this.receiveWindowSize
)
if (this.sessionFlowController !== undefined) {
this.sessionFlowController.ensureWindowAtLeast(
BigInt(
FlowController.kSessionFlowControlMultiplier *
Number(this.receiveWindowSize)
)
)
}
} else {
// TODO(ckrasic) - add a varz to track this (?).
log(
'Max window at limit for ' +
' after ' +
sinceLast +
' us, and RTT is ' +
rtt +
'us. Limit size: ' +
this.receiveWindowSize
)
}
}
increaseWindowSize() {
this.receiveWindowSize *= 2n
this.receiveWindowSize =
this.receiveWindowSize > this.receiveWindowSizeLimit
? this.receiveWindowSizeLimit
: this.receiveWindowSize
}
windowUpdateThreshold() {
return this.receiveWindowSize / 2n
}
maybeSendWindowUpdate() {
if (!this.tocontrol.connected()) {
return
}
// Send WindowUpdate to increase receive window if
// (receive window offset - consumed bytes) < (max window / 2).
// This is behaviour copied from SPDY.
const availableWindow = this.receiveWindowOffset - this.bytesConsumed
const threshold = this.windowUpdateThreshold()
if (!this.prevWindowUpdateTime) {
// Treat the initial window as if it is a window update, so if 1/2 the
// window is used in less than 2 RTTs, the window is increased.
this.prevWindowUpdateTime = Date.now()
}
if (availableWindow >= threshold) {
log(
'Not sending WindowUpdate for ' +
', available window: ' +
availableWindow +
' >= threshold: ' +
threshold
)
return
}
this.maybeIncreaseMaxWindowSize()
this.updateReceiveWindowOffsetAndSendWindowUpdate(availableWindow)
}
/**
* @param {bigint} availableWindow
*/
updateReceiveWindowOffsetAndSendWindowUpdate(availableWindow) {
// Update our receive window.
this.receiveWindowOffset += this.receiveWindowSize - availableWindow
log(
'Sending WindowUpdate frame for ' +
', consumed bytes: ' +
this.bytesConsumed +
', available window: ' +
availableWindow +
', and threshold: ' +
this.windowUpdateThreshold() +
', and receive window size: ' +
this.receiveWindowSize +
'. New receive window offset is: ' +
this.receiveWindowOffset
)
this.sendWindowUpdate()
}
maybeSendBlocked() {
if (
this.sendWindowSize() !== 0n ||
this.lastBlockedSendWindowOffset >= this.sendWindowOffset
) {
return
}
log(
' is flow control blocked. ' +
'Send window: ' +
this.sendWindowSize() +
', bytes sent: ' +
this.bytesSent +
', send limit: ' +
this.sendWindowOffset
)
// The entire send_window has been consumed, we are now flow control
// blocked.
// Keep track of when we last sent a BLOCKED frame so that we only send one
// at a given send offset.
this.lastBlockedSendWindowOffset = this.sendWindowOffset
this.tocontrol.sendBlocked(this.lastBlockedSendWindowOffset)
}
/**
* @param {bigint} newSendWindowOffset
*/
updateSendWindowOffset(newSendWindowOffset) {
// Only update if send window has increased.
if (newSendWindowOffset <= this.sendWindowOffset) {
return false
}
log(
'UpdateSendWindowOffset for ' +
' with new offset ' +
newSendWindowOffset +
' current offset: ' +
this.sendWindowOffset +
' bytes_sent: ' +
this.bytesSent
)
// The flow is now unblocked but could have also been unblocked
// before. Return true iff this update caused a change from blocked
// to unblocked.
const wasPreviouslyBlocked = this.isBlocked()
this.sendWindowOffset = newSendWindowOffset
return wasPreviouslyBlocked
}
/**
* @param {bigint} windowSize
*/
ensureWindowAtLeast(windowSize) {
if (this.receiveWindowSizeLimit >= windowSize) {
return
}
const availableWindow = this.receiveWindowOffset - this.bytesConsumed
this.increaseWindowSize()
this.updateReceiveWindowOffsetAndSendWindowUpdate(availableWindow)
}
isBlocked() {
return this.sendWindowSize() === 0n
}
sendWindowSize() {
if (this.bytesSent > this.sendWindowOffset) {
return 0n
}
return this.sendWindowOffset - this.bytesSent
}
/**
* @param {bigint} size
*/
updateReceiveWindowSize(size) {
log('UpdateReceiveWindowSize for ' + ': ' + size)
if (this.receiveWindowSize !== this.receiveWindowOffset) {
log(
'receive_window_size_:' +
this.receiveWindowSize +
' != receive_window_offset:' +
this.receiveWindowOffset
)
return
}
this.receiveWindowSize = size
this.receiveWindowOffset = size
}
sendWindowUpdate() {
this.tocontrol.sendWindowUpdate(this.receiveWindowOffset)
}
}