@amritk/nish-x86_64-linux 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +43 -34
- package/bin/nish +0 -0
- package/package.json +1 -1
- package/runtime/nish.d.ts +117 -8
- package/runtime/nish.h +50 -13
- package/runtime/nish.mjs +71 -7
- package/runtime/runtime-host.c +43 -24
- package/runtime/runtime-net.c +57 -10
- package/runtime/runtime-os.c +62 -20
- package/runtime/runtime-simd.c +203 -0
- package/runtime/runtime-wasm.c +12 -0
- package/runtime/runtime.c +66 -18
- package/runtime/shim.mjs +114 -5
- package/scripts/build.sh +14 -12
- package/std/README.md +168 -36
- package/std/crypto/aes.ts +51 -8
- package/std/crypto/base64url.ts +25 -12
- package/std/crypto/chacha20poly1305.ts +43 -29
- package/std/crypto/ct.ts +6 -1
- package/std/crypto/hkdf.ts +364 -3
- package/std/crypto/hmac.ts +307 -7
- package/std/crypto/p256.ts +618 -34
- package/std/crypto/sha1.ts +147 -0
- package/std/crypto/sha256.ts +3 -2
- package/std/crypto/x25519.ts +88 -36
- package/std/crypto/x509.ts +130 -47
- package/std/json.ts +297 -41
- package/std/net/hpack.ts +1094 -0
- package/std/net/http-fields.ts +604 -0
- package/std/net/http1-server.ts +1389 -0
- package/std/net/http1.ts +1492 -0
- package/std/net/http2-frame.ts +745 -0
- package/std/net/http2-tls.ts +216 -0
- package/std/net/http2.ts +1433 -0
- package/std/net/http3-frame.ts +457 -0
- package/std/net/http3-server.ts +911 -0
- package/std/net/http3.ts +2145 -0
- package/std/net/qpack.ts +1163 -0
- package/std/net/quic-conn-ack.ts +206 -0
- package/std/net/quic-conn-cid.ts +425 -0
- package/std/net/quic-conn-params.ts +462 -0
- package/std/net/quic-datagram.ts +130 -0
- package/std/net/quic-frame.ts +1206 -0
- package/std/net/quic-listener.ts +929 -0
- package/std/net/quic-packet.ts +1698 -0
- package/std/net/quic-recovery.ts +849 -0
- package/std/net/quic-stream.ts +1609 -0
- package/std/net/quic.ts +3578 -0
- package/std/net/tls/codec.ts +1089 -0
- package/std/net/tls/record-server.ts +1173 -0
- package/std/net/tls/record.ts +621 -0
- package/std/net/tls/schedule.ts +429 -0
- package/std/net/tls-tcp.ts +414 -0
- package/std/net/tls.ts +1437 -0
- package/std/net/websocket.ts +823 -0
- package/std/net/webtransport.ts +1334 -0
- package/std/secret.ts +62 -0
- package/std/text.ts +77 -0
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nish/net/quic-recovery` — QUIC loss detection and congestion control
|
|
3
|
+
* (RFC 9002): the packets each packet number space has in flight, the
|
|
4
|
+
* round-trip estimate, loss by packet and by time threshold, the probe
|
|
5
|
+
* timeout with its backoff, NewReno, and a pacer. Sans-IO: it never sees a
|
|
6
|
+
* byte of a packet, only packet numbers, sizes and the caller's clock.
|
|
7
|
+
*
|
|
8
|
+
* import { QuicRecovery, QUIC_RECOVERY_TIMEOUT_PTO } from "nish/net/quic-recovery";
|
|
9
|
+
*
|
|
10
|
+
* const recovery = new QuicRecovery();
|
|
11
|
+
* const slot: i32 = recovery.onPacketSent(space, pn, size, now); // every ack-eliciting packet
|
|
12
|
+
* recovery.onAck(space, frame.ackRanges, frame.ackRangeCount, delay, now);
|
|
13
|
+
* … recovery.spaces[space].acked / .lost name the slots it settled …
|
|
14
|
+
* if (now >= recovery.deadline(blocked)) { recovery.onTimeout(now, blocked); }
|
|
15
|
+
*
|
|
16
|
+
* **What it tracks.** Each space keeps its ack-eliciting packets in a fixed
|
|
17
|
+
* ring (`QuicSentPackets`), in the order they were sent, so the memory is
|
|
18
|
+
* fixed when the connection is made and nothing is allocated per packet. A
|
|
19
|
+
* packet that elicits no acknowledgement (one that carries only ACK, PADDING
|
|
20
|
+
* or CONNECTION_CLOSE) is not recorded: it is never in flight, and neither
|
|
21
|
+
* an RTT sample nor a loss is ever taken from it (RFC 9002 §2, §5.1). The
|
|
22
|
+
* caller keeps what each packet carried in its own arrays, indexed by the
|
|
23
|
+
* slot `onPacketSent` answers, and reads `acked` and `lost` after each
|
|
24
|
+
* `onAck` and `onTimeout` to learn which slots were settled. A slot named
|
|
25
|
+
* there keeps its packet number, time and size until the next call.
|
|
26
|
+
*
|
|
27
|
+
* **Time and units.** Every time and every duration is the caller's
|
|
28
|
+
* monotonic clock in milliseconds, which is also the timer granularity
|
|
29
|
+
* (`QUIC_RECOVERY_GRANULARITY`, §6.1.2), and the arithmetic is Appendix A's
|
|
30
|
+
* and Appendix B's in integers: `smoothed_rtt = (7 × smoothed_rtt +
|
|
31
|
+
* adjusted_rtt) / 8`, `rttvar = (3 × rttvar + |smoothed_rtt −
|
|
32
|
+
* adjusted_rtt|) / 4`, each division truncating. Sizes are bytes.
|
|
33
|
+
*
|
|
34
|
+
* **What the server leaves out.** It is written for the server side of a
|
|
35
|
+
* connection: there is no client anti-deadlock probe (§6.2.2.1), since a
|
|
36
|
+
* server never has to send to unblock its peer, and nothing reads ECN
|
|
37
|
+
* (§7.1, B.7). A congestion window that the sender does not use is not
|
|
38
|
+
* grown (§7.8): an acknowledgement grows it only if, when it arrived, the
|
|
39
|
+
* bytes in flight were at least half the window.
|
|
40
|
+
*
|
|
41
|
+
* **What a peer cannot do.** Every input is checked: the ranges of an
|
|
42
|
+
* acknowledgement are read only up to the count given and the array's
|
|
43
|
+
* length, a packet acknowledged twice counts once, and
|
|
44
|
+
* the backoff stops doubling at `QUIC_RECOVERY_MAX_BACKOFF`, so no duration
|
|
45
|
+
* overflows. A peer that never acknowledges anything costs a probe each
|
|
46
|
+
* timeout, at a rate that halves each time, until the idle timeout ends the
|
|
47
|
+
* connection.
|
|
48
|
+
*
|
|
49
|
+
* Written from RFC 9002 §5 to §7 and its Appendices A and B, in this
|
|
50
|
+
* module's own structure; nothing here is ported from another
|
|
51
|
+
* implementation. Private names carry the `quicRecovery` prefix
|
|
52
|
+
* (`docs/wp26-stdlib.md` §3e).
|
|
53
|
+
*/
|
|
54
|
+
import { QUIC_MAX_VARINT } from "nish/net/quic-packet"
|
|
55
|
+
|
|
56
|
+
/** The Initial space's index, which is `TLS_LEVEL_INITIAL`. */
|
|
57
|
+
export const QUIC_RECOVERY_INITIAL: i32 = 0
|
|
58
|
+
/** The Handshake space's index. */
|
|
59
|
+
export const QUIC_RECOVERY_HANDSHAKE: i32 = 1
|
|
60
|
+
/** The Application Data space's index. */
|
|
61
|
+
export const QUIC_RECOVERY_APPLICATION: i32 = 2
|
|
62
|
+
/**
|
|
63
|
+
* How many packets the Initial and the Handshake space each keep in flight.
|
|
64
|
+
* A server's handshake flight is a few datagrams, and the anti-amplification
|
|
65
|
+
* limit holds it to three times what the client sent until its address is
|
|
66
|
+
* validated, so 16 leaves room for several probes on top.
|
|
67
|
+
*/
|
|
68
|
+
export const QUIC_RECOVERY_HANDSHAKE_CAPACITY: i32 = 16
|
|
69
|
+
/**
|
|
70
|
+
* How many packets the Application Data space keeps in flight: 128
|
|
71
|
+
* full-sized datagrams is 150 KiB in flight, a 12 Mbit/s path at a 100 ms
|
|
72
|
+
* round trip, against what the record costs every connection from the
|
|
73
|
+
* start. When the ring is full the space sends nothing that would elicit an
|
|
74
|
+
* acknowledgement until one arrives, as if the congestion window were full.
|
|
75
|
+
*/
|
|
76
|
+
export const QUIC_RECOVERY_APPLICATION_CAPACITY: i32 = 128
|
|
77
|
+
/** kPacketThreshold (§6.1.1): a packet three numbers below one acknowledged is lost. */
|
|
78
|
+
export const QUIC_RECOVERY_PACKET_THRESHOLD: i64 = 3
|
|
79
|
+
/** kGranularity (§6.1.2), in milliseconds: the timer resolution, and the least any loss delay or variance term is. */
|
|
80
|
+
export const QUIC_RECOVERY_GRANULARITY: i64 = 1
|
|
81
|
+
/** kInitialRtt (§6.2.2), in milliseconds: the round trip assumed before the first sample. */
|
|
82
|
+
export const QUIC_RECOVERY_INITIAL_RTT: i64 = 333
|
|
83
|
+
/** max_datagram_size (B.1): the 1200 bytes every datagram this server sends is at most. */
|
|
84
|
+
export const QUIC_RECOVERY_DATAGRAM_SIZE: i64 = 1200
|
|
85
|
+
/** kInitialWindow (§7.2): min(10 × 1200, max(14720, 2 × 1200)), so 12000 bytes. */
|
|
86
|
+
export const QUIC_RECOVERY_INITIAL_WINDOW: i64 = 12000
|
|
87
|
+
/** kMinimumWindow (§7.2): two datagrams, 2400 bytes. */
|
|
88
|
+
export const QUIC_RECOVERY_MINIMUM_WINDOW: i64 = 2400
|
|
89
|
+
/** kPersistentCongestionThreshold (§7.6.1): lost packets spanning three probe timeouts collapse the window. */
|
|
90
|
+
export const QUIC_RECOVERY_PERSISTENT_THRESHOLD: i64 = 3
|
|
91
|
+
/** The default max_ack_delay (RFC 9000 §18.2), in milliseconds, until the peer's transport parameters say otherwise. */
|
|
92
|
+
export const QUIC_RECOVERY_DEFAULT_MAX_ACK_DELAY: i64 = 25
|
|
93
|
+
/**
|
|
94
|
+
* The most times the probe timeout doubles (§6.2.1). Sixteen doublings of
|
|
95
|
+
* the longest probe timeout a peer can cause still fit an `i64`, and by then
|
|
96
|
+
* the idle timeout has ended the connection.
|
|
97
|
+
*/
|
|
98
|
+
export const QUIC_RECOVERY_MAX_BACKOFF: i32 = 16
|
|
99
|
+
|
|
100
|
+
/** `onTimeout` ran nothing: no timer was due. */
|
|
101
|
+
export const QUIC_RECOVERY_TIMEOUT_NONE: i32 = 0
|
|
102
|
+
/** `onTimeout` declared packets lost by the time threshold; `lost` in `timeoutSpace` names them. */
|
|
103
|
+
export const QUIC_RECOVERY_TIMEOUT_LOSS: i32 = 1
|
|
104
|
+
/** `onTimeout` was a probe timeout: the caller sends a probe in `timeoutSpace` (§6.2.4). */
|
|
105
|
+
export const QUIC_RECOVERY_TIMEOUT_PTO: i32 = 2
|
|
106
|
+
|
|
107
|
+
/** A slot's states: free, a packet in flight, one acknowledged, one declared lost, and one this call declared lost. */
|
|
108
|
+
const QUIC_RECOVERY_FREE: i32 = 0
|
|
109
|
+
const QUIC_RECOVERY_SENT: i32 = 1
|
|
110
|
+
const QUIC_RECOVERY_ACKED: i32 = 2
|
|
111
|
+
const QUIC_RECOVERY_LOST: i32 = 3
|
|
112
|
+
const QUIC_RECOVERY_LOST_NOW: i32 = 4
|
|
113
|
+
|
|
114
|
+
/** The larger of two times or durations. */
|
|
115
|
+
const quicRecoveryMax = (a: i64, b: i64): i64 => (a > b ? a : b)
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The ack-eliciting packets one space sent, in a ring of `capacity` slots
|
|
119
|
+
* in the order they went out: slot `slot(k)` is the `k`-th from the oldest
|
|
120
|
+
* still kept, for `k` below `count`. A packet stays in its slot after it is
|
|
121
|
+
* acknowledged or lost until every older one is settled too, which is what
|
|
122
|
+
* lets persistent congestion see that nothing between two lost packets was
|
|
123
|
+
* acknowledged (§7.6.2).
|
|
124
|
+
*/
|
|
125
|
+
export class QuicSentPackets {
|
|
126
|
+
pn: i64[]
|
|
127
|
+
timeSent: i64[]
|
|
128
|
+
size: i32[]
|
|
129
|
+
state: u8[]
|
|
130
|
+
/** The slots the last `onAck` acknowledged, `ackedCount` of them, oldest first. */
|
|
131
|
+
acked: i32[]
|
|
132
|
+
/** The slots the last `onAck` or `onTimeout` declared lost, `lostCount` of them, oldest first. */
|
|
133
|
+
lost: i32[]
|
|
134
|
+
/** The largest packet number the peer acknowledged in this space, or -1. */
|
|
135
|
+
largestAcked: i64 = -1
|
|
136
|
+
/** The packet number of the last packet recorded, or -1: each must be larger (RFC 9000 §12.3). */
|
|
137
|
+
lastPn: i64 = -1
|
|
138
|
+
/** loss_time (A.3): when the oldest packet not yet lost by the packet threshold becomes lost by time, or -1. */
|
|
139
|
+
lossTime: i64 = -1
|
|
140
|
+
/** time_of_last_ack_eliciting_packet (A.3), or -1 before the first. */
|
|
141
|
+
lastAckEliciting: i64 = -1
|
|
142
|
+
/** The bytes of this space's packets in flight. */
|
|
143
|
+
bytesInFlight: i64 = 0
|
|
144
|
+
capacity: i32 = 0
|
|
145
|
+
head: i32 = 0
|
|
146
|
+
count: i32 = 0
|
|
147
|
+
/** How many recorded packets are still in flight: neither acknowledged nor lost. */
|
|
148
|
+
inFlight: i32 = 0
|
|
149
|
+
ackedCount: i32 = 0
|
|
150
|
+
lostCount: i32 = 0
|
|
151
|
+
|
|
152
|
+
constructor(capacity: i32) {
|
|
153
|
+
this.capacity = capacity
|
|
154
|
+
this.pn = new Array<i64>(capacity)
|
|
155
|
+
this.timeSent = new Array<i64>(capacity)
|
|
156
|
+
this.size = new Array<i32>(capacity)
|
|
157
|
+
this.state = new Array<u8>(capacity)
|
|
158
|
+
this.acked = new Array<i32>(capacity)
|
|
159
|
+
this.lost = new Array<i32>(capacity)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The ring slot of the `k`-th packet kept, counting from the oldest. */
|
|
163
|
+
slot(k: i32): i32 {
|
|
164
|
+
return (this.head + k) % this.capacity
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The state of slot `s`, or free for a slot outside the ring. */
|
|
168
|
+
stateAt(s: i32): i32 {
|
|
169
|
+
if (s < 0 || s >= toI32(this.state.length)) {
|
|
170
|
+
return QUIC_RECOVERY_FREE
|
|
171
|
+
}
|
|
172
|
+
return toI32(this.state[s])
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Sets slot `s`'s state. */
|
|
176
|
+
setState(s: i32, value: i32): void {
|
|
177
|
+
if (s >= 0 && s < toI32(this.state.length)) {
|
|
178
|
+
this.state[s] = toU8(value)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The packet number in slot `s`, or -1. */
|
|
183
|
+
pnAt(s: i32): i64 {
|
|
184
|
+
return s >= 0 && s < toI32(this.pn.length) ? this.pn[s] : -1
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** When the packet in slot `s` was sent, or -1. */
|
|
188
|
+
timeAt(s: i32): i64 {
|
|
189
|
+
return s >= 0 && s < toI32(this.timeSent.length) ? this.timeSent[s] : -1
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The size of the packet in slot `s`, or 0. */
|
|
193
|
+
sizeAt(s: i32): i32 {
|
|
194
|
+
return s >= 0 && s < toI32(this.size.length) ? this.size[s] : 0
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The `k`-th slot the last call acknowledged, or -1. */
|
|
198
|
+
ackedSlot(k: i32): i32 {
|
|
199
|
+
return k >= 0 && k < this.ackedCount && k < toI32(this.acked.length) ? this.acked[k] : -1
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The `k`-th slot the last call declared lost, or -1. */
|
|
203
|
+
lostSlot(k: i32): i32 {
|
|
204
|
+
return k >= 0 && k < this.lostCount && k < toI32(this.lost.length) ? this.lost[k] : -1
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Whether slot `s` holds a packet still in flight. */
|
|
208
|
+
inFlightAt(s: i32): boolean {
|
|
209
|
+
return this.stateAt(s) === QUIC_RECOVERY_SENT
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Whether every slot is taken, so nothing more can be recorded until a packet is settled. */
|
|
213
|
+
full(): boolean {
|
|
214
|
+
this.compact()
|
|
215
|
+
return this.count >= this.capacity
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Frees the oldest slots that are settled, acknowledged or lost. */
|
|
219
|
+
compact(): void {
|
|
220
|
+
while (this.count > 0 && this.stateAt(this.head) !== QUIC_RECOVERY_SENT) {
|
|
221
|
+
this.setState(this.head, QUIC_RECOVERY_FREE)
|
|
222
|
+
this.head = (this.head + 1) % this.capacity
|
|
223
|
+
this.count = this.count - 1
|
|
224
|
+
}
|
|
225
|
+
if (this.count === 0) {
|
|
226
|
+
this.head = 0
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Forgets the results of the last call. */
|
|
231
|
+
clearResults(): void {
|
|
232
|
+
this.ackedCount = 0
|
|
233
|
+
this.lostCount = 0
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Takes packet `s` out of flight. */
|
|
237
|
+
settle(s: i32, state: i32): void {
|
|
238
|
+
this.setState(s, state)
|
|
239
|
+
this.inFlight = this.inFlight - 1
|
|
240
|
+
this.bytesInFlight = this.bytesInFlight - toI64(this.sizeAt(s))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Settles packet `s` as lost and lists it in `lost`. */
|
|
244
|
+
markLost(s: i32): void {
|
|
245
|
+
this.settle(s, QUIC_RECOVERY_LOST_NOW)
|
|
246
|
+
if (this.lostCount < toI32(this.lost.length)) {
|
|
247
|
+
this.lost[this.lostCount] = s
|
|
248
|
+
this.lostCount = this.lostCount + 1
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Drops every packet, as when the space's keys are discarded (A.11). */
|
|
253
|
+
clear(): void {
|
|
254
|
+
for (let s: i32 = 0; s < toI32(this.state.length); s += 1) {
|
|
255
|
+
this.state[s] = toU8(QUIC_RECOVERY_FREE)
|
|
256
|
+
}
|
|
257
|
+
this.head = 0
|
|
258
|
+
this.count = 0
|
|
259
|
+
this.inFlight = 0
|
|
260
|
+
this.bytesInFlight = 0
|
|
261
|
+
this.lossTime = -1
|
|
262
|
+
this.lastAckEliciting = -1
|
|
263
|
+
this.clearResults()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Empties the ring and forgets every number, for a connection slot reused for another peer. */
|
|
267
|
+
reset(): void {
|
|
268
|
+
this.clear()
|
|
269
|
+
this.largestAcked = -1
|
|
270
|
+
this.lastPn = -1
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Whether `pn` falls in one of the first `count` `[smallest, largest]` pairs of `ranges`. */
|
|
275
|
+
const quicRecoveryInRanges = (ranges: i64[], count: i32, pn: i64): boolean => {
|
|
276
|
+
for (let k: i32 = 0; k < count; k += 1) {
|
|
277
|
+
const low: i32 = k * 2
|
|
278
|
+
const high: i32 = low + 1
|
|
279
|
+
if (low < 0 || low >= toI32(ranges.length) || high < 0 || high >= toI32(ranges.length)) {
|
|
280
|
+
return false
|
|
281
|
+
}
|
|
282
|
+
if (pn >= ranges[low] && pn <= ranges[high]) {
|
|
283
|
+
return true
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return false
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* One connection's loss recovery and congestion control, across its three
|
|
291
|
+
* packet number spaces. The fields are readable: the RTT estimate
|
|
292
|
+
* (`latestRtt`, `smoothedRtt`, `rttVar`, `minRtt`), the window
|
|
293
|
+
* (`congestionWindow`, `ssthresh`, `recoveryStart`), `ptoCount`, and the
|
|
294
|
+
* counts a test or a log reads (`congestionEvents`, `persistentCongestions`).
|
|
295
|
+
*/
|
|
296
|
+
export class QuicRecovery {
|
|
297
|
+
spaces: QuicSentPackets[]
|
|
298
|
+
/** latest_rtt (§5.1): the last sample, in milliseconds. */
|
|
299
|
+
latestRtt: i64 = 0
|
|
300
|
+
/** smoothed_rtt (§5.3): kInitialRtt until the first sample. */
|
|
301
|
+
smoothedRtt: i64 = 0
|
|
302
|
+
/** rttvar (§5.3): kInitialRtt / 2 until the first sample. */
|
|
303
|
+
rttVar: i64 = 0
|
|
304
|
+
/** min_rtt (§5.2): 0 until the first sample. */
|
|
305
|
+
minRtt: i64 = 0
|
|
306
|
+
/** When the first RTT sample was taken, or -1 before it: persistent congestion counts only packets sent since (§7.6.2). */
|
|
307
|
+
firstSampleTime: i64 = -1
|
|
308
|
+
/** The peer's max_ack_delay in milliseconds (RFC 9000 §18.2), which the caller sets once it has read it. */
|
|
309
|
+
maxAckDelay: i64 = 0
|
|
310
|
+
/** congestion_window (B.2), in bytes. */
|
|
311
|
+
congestionWindow: i64 = 0
|
|
312
|
+
/** ssthresh (B.2): unbounded (2^62 − 1) until the first congestion event. */
|
|
313
|
+
ssthresh: i64 = 0
|
|
314
|
+
/** congestion_recovery_start_time (B.2), or -1 outside recovery. */
|
|
315
|
+
recoveryStart: i64 = -1
|
|
316
|
+
/** The pacer's credit in bytes (§7.7), and the time it was last topped up, or -1. */
|
|
317
|
+
pacerBudget: i64 = 0
|
|
318
|
+
pacerTime: i64 = -1
|
|
319
|
+
/** pto_count (A.3): how many probe timeouts fired since the last acknowledgement. */
|
|
320
|
+
ptoCount: i32 = 0
|
|
321
|
+
/** The space the last `onTimeout` acted on, or -1. */
|
|
322
|
+
timeoutSpace: i32 = -1
|
|
323
|
+
/** How many congestion events reduced the window, and how many of them were persistent congestion. */
|
|
324
|
+
congestionEvents: i32 = 0
|
|
325
|
+
persistentCongestions: i32 = 0
|
|
326
|
+
/** Whether the handshake is confirmed (RFC 9001 §4.1.2), which the caller sets: the peer's max_ack_delay then bounds its ACK delays, and the Application Data space has a probe timeout. */
|
|
327
|
+
handshakeConfirmed: boolean = false
|
|
328
|
+
|
|
329
|
+
constructor() {
|
|
330
|
+
this.spaces = [
|
|
331
|
+
new QuicSentPackets(QUIC_RECOVERY_HANDSHAKE_CAPACITY),
|
|
332
|
+
new QuicSentPackets(QUIC_RECOVERY_HANDSHAKE_CAPACITY),
|
|
333
|
+
new QuicSentPackets(QUIC_RECOVERY_APPLICATION_CAPACITY),
|
|
334
|
+
]
|
|
335
|
+
this.reset()
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Puts everything back as a new connection has it: every space empty, the
|
|
340
|
+
* RTT estimate and the window at their initial values. A connection slot
|
|
341
|
+
* reused for another peer calls it; the rings are kept.
|
|
342
|
+
*/
|
|
343
|
+
reset(): void {
|
|
344
|
+
for (const sp of this.spaces) {
|
|
345
|
+
sp.reset()
|
|
346
|
+
}
|
|
347
|
+
this.latestRtt = 0
|
|
348
|
+
this.smoothedRtt = QUIC_RECOVERY_INITIAL_RTT
|
|
349
|
+
this.rttVar = QUIC_RECOVERY_INITIAL_RTT / 2
|
|
350
|
+
this.minRtt = 0
|
|
351
|
+
this.firstSampleTime = -1
|
|
352
|
+
this.maxAckDelay = QUIC_RECOVERY_DEFAULT_MAX_ACK_DELAY
|
|
353
|
+
this.congestionWindow = QUIC_RECOVERY_INITIAL_WINDOW
|
|
354
|
+
this.ssthresh = QUIC_MAX_VARINT
|
|
355
|
+
this.recoveryStart = -1
|
|
356
|
+
this.pacerBudget = QUIC_RECOVERY_INITIAL_WINDOW
|
|
357
|
+
this.pacerTime = -1
|
|
358
|
+
this.ptoCount = 0
|
|
359
|
+
this.timeoutSpace = -1
|
|
360
|
+
this.congestionEvents = 0
|
|
361
|
+
this.persistentCongestions = 0
|
|
362
|
+
this.handshakeConfirmed = false
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Space `space`'s packets, or `null` for an index that names no space. */
|
|
366
|
+
space(space: i32): QuicSentPackets | null {
|
|
367
|
+
if (space < 0 || space >= toI32(this.spaces.length)) {
|
|
368
|
+
return null
|
|
369
|
+
}
|
|
370
|
+
return this.spaces[space]
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** bytes_in_flight (B.2): every space's packets in flight. */
|
|
374
|
+
bytesInFlight(): i64 {
|
|
375
|
+
let total: i64 = 0
|
|
376
|
+
for (const sp of this.spaces) {
|
|
377
|
+
total = total + sp.bytesInFlight
|
|
378
|
+
}
|
|
379
|
+
return total
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Whether the window has room for another full-sized datagram (B.4). The
|
|
384
|
+
* caller sends nothing ack-eliciting while it has not, except a probe.
|
|
385
|
+
*/
|
|
386
|
+
canSend(): boolean {
|
|
387
|
+
return this.bytesInFlight() + QUIC_RECOVERY_DATAGRAM_SIZE <= this.congestionWindow
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Records an ack-eliciting packet (A.5): number `pn` of space `space`,
|
|
392
|
+
* `size` bytes on the wire, sent at `now`. Answers its slot, under which
|
|
393
|
+
* the caller keeps what it carried, or -1, recording nothing, for a space
|
|
394
|
+
* that does not exist, a size that is not positive, a packet number that is
|
|
395
|
+
* not above the last one recorded, or a ring that is full.
|
|
396
|
+
*/
|
|
397
|
+
onPacketSent(space: i32, pn: i64, size: i32, now: i64): i32 {
|
|
398
|
+
const sp: QuicSentPackets | null = this.space(space)
|
|
399
|
+
if (sp === null || size <= 0 || pn <= sp.lastPn || pn > QUIC_MAX_VARINT || sp.full()) {
|
|
400
|
+
return -1
|
|
401
|
+
}
|
|
402
|
+
const s: i32 = sp.slot(sp.count)
|
|
403
|
+
if (s < 0 || s >= toI32(sp.pn.length) || s >= toI32(sp.timeSent.length) || s >= toI32(sp.size.length)) {
|
|
404
|
+
return -1
|
|
405
|
+
}
|
|
406
|
+
sp.pn[s] = pn
|
|
407
|
+
sp.timeSent[s] = now
|
|
408
|
+
sp.size[s] = size
|
|
409
|
+
sp.setState(s, QUIC_RECOVERY_SENT)
|
|
410
|
+
sp.count = sp.count + 1
|
|
411
|
+
sp.inFlight = sp.inFlight + 1
|
|
412
|
+
sp.bytesInFlight = sp.bytesInFlight + toI64(size)
|
|
413
|
+
sp.lastPn = pn
|
|
414
|
+
sp.lastAckEliciting = now
|
|
415
|
+
return s
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* An ACK frame for space `space` (A.7): `ranges` holds `count`
|
|
420
|
+
* `[smallest, largest]` pairs, highest first, as `QuicFrame.ackRanges`
|
|
421
|
+
* does, and `ackDelay` is the peer's ACK Delay in milliseconds. The
|
|
422
|
+
* packets newly acknowledged are listed in the space's `acked`, those it
|
|
423
|
+
* shows lost in `lost`; an RTT sample is taken when the largest
|
|
424
|
+
* acknowledged is newly acknowledged (§5.1), the window grows or shrinks
|
|
425
|
+
* (B.5, B.8), and the probe backoff resets. Answers how many packets were
|
|
426
|
+
* newly acknowledged, or -1, changing nothing, when the space does not
|
|
427
|
+
* exist or there is no range. A packet number the space never sent is the
|
|
428
|
+
* caller's to refuse (RFC 9000 §13.1): only it knows the packets that
|
|
429
|
+
* elicit no acknowledgement, which are not recorded here.
|
|
430
|
+
*/
|
|
431
|
+
onAck(space: i32, ranges: i64[], count: i32, ackDelay: i64, now: i64): i32 {
|
|
432
|
+
const sp: QuicSentPackets | null = this.space(space)
|
|
433
|
+
if (sp === null || count <= 0 || toI32(ranges.length) < 2) {
|
|
434
|
+
return -1
|
|
435
|
+
}
|
|
436
|
+
const largest: i64 = ranges[1]
|
|
437
|
+
sp.compact()
|
|
438
|
+
sp.clearResults()
|
|
439
|
+
if (largest > sp.largestAcked) {
|
|
440
|
+
sp.largestAcked = largest
|
|
441
|
+
}
|
|
442
|
+
// The bytes in flight before this ACK decide whether the window was in
|
|
443
|
+
// use, and so whether it may grow (§7.8).
|
|
444
|
+
const inUse: boolean = this.bytesInFlight() * 2 >= this.congestionWindow
|
|
445
|
+
let sampleAt: i32 = -1
|
|
446
|
+
for (let k: i32 = 0; k < sp.count; k += 1) {
|
|
447
|
+
const s: i32 = sp.slot(k)
|
|
448
|
+
// The ring is in packet-number order, so nothing past the largest acknowledged is.
|
|
449
|
+
if (sp.pnAt(s) > largest) {
|
|
450
|
+
break
|
|
451
|
+
}
|
|
452
|
+
if (sp.stateAt(s) === QUIC_RECOVERY_SENT && quicRecoveryInRanges(ranges, count, sp.pnAt(s))) {
|
|
453
|
+
sp.settle(s, QUIC_RECOVERY_ACKED)
|
|
454
|
+
if (sp.ackedCount < toI32(sp.acked.length)) {
|
|
455
|
+
sp.acked[sp.ackedCount] = s
|
|
456
|
+
sp.ackedCount = sp.ackedCount + 1
|
|
457
|
+
}
|
|
458
|
+
if (sp.pnAt(s) === largest) {
|
|
459
|
+
sampleAt = s
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (sp.ackedCount === 0) {
|
|
464
|
+
return 0
|
|
465
|
+
}
|
|
466
|
+
if (sampleAt >= 0) {
|
|
467
|
+
this.latestRtt = quicRecoveryMax(now - sp.timeAt(sampleAt), 0)
|
|
468
|
+
// §5.3: the Initial space's ACKs are not delayed by the peer, so their delay is ignored.
|
|
469
|
+
const none: i64 = 0
|
|
470
|
+
this.updateRtt(space === QUIC_RECOVERY_INITIAL ? none : quicRecoveryMax(ackDelay, none), now)
|
|
471
|
+
}
|
|
472
|
+
this.detectLoss(sp, now)
|
|
473
|
+
this.packetsLost(sp, now)
|
|
474
|
+
for (let k: i32 = 0; k < sp.ackedCount; k += 1) {
|
|
475
|
+
this.packetAcked(sp, sp.ackedSlot(k), inUse)
|
|
476
|
+
}
|
|
477
|
+
this.ptoCount = 0
|
|
478
|
+
return sp.ackedCount
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** UpdateRtt (§5.3, A.7) with a sample in `latestRtt`, `ackDelay` the peer's delay in milliseconds. */
|
|
482
|
+
updateRtt(ackDelay: i64, now: i64): void {
|
|
483
|
+
const latest: i64 = this.latestRtt
|
|
484
|
+
if (this.firstSampleTime < 0) {
|
|
485
|
+
this.minRtt = latest
|
|
486
|
+
this.smoothedRtt = latest
|
|
487
|
+
this.rttVar = latest / 2
|
|
488
|
+
this.firstSampleTime = now
|
|
489
|
+
return
|
|
490
|
+
}
|
|
491
|
+
if (latest < this.minRtt) {
|
|
492
|
+
this.minRtt = latest
|
|
493
|
+
}
|
|
494
|
+
let delay: i64 = ackDelay
|
|
495
|
+
if (this.handshakeConfirmed && delay > this.maxAckDelay) {
|
|
496
|
+
delay = this.maxAckDelay
|
|
497
|
+
}
|
|
498
|
+
// The delay is taken off only where that leaves the sample at or above min_rtt.
|
|
499
|
+
let adjusted: i64 = latest
|
|
500
|
+
if (latest >= this.minRtt + delay) {
|
|
501
|
+
adjusted = latest - delay
|
|
502
|
+
}
|
|
503
|
+
const gap: i64 = this.smoothedRtt > adjusted ? this.smoothedRtt - adjusted : adjusted - this.smoothedRtt
|
|
504
|
+
this.rttVar = (3 * this.rttVar + gap) / 4
|
|
505
|
+
this.smoothedRtt = (7 * this.smoothedRtt + adjusted) / 8
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** The loss delay (§6.1.2): 9/8 of the larger of the latest and the smoothed RTT, at least the granularity. */
|
|
509
|
+
lossDelay(): i64 {
|
|
510
|
+
const delay: i64 = (9 * quicRecoveryMax(this.latestRtt, this.smoothedRtt)) / 8
|
|
511
|
+
return quicRecoveryMax(delay, QUIC_RECOVERY_GRANULARITY)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* DetectAndRemoveLostPackets (A.10): every packet in flight at or below
|
|
516
|
+
* the largest acknowledged is lost once it was sent a loss delay ago, or
|
|
517
|
+
* three numbers below the largest acknowledged; the first that is neither
|
|
518
|
+
* sets `lossTime`.
|
|
519
|
+
*/
|
|
520
|
+
detectLoss(sp: QuicSentPackets, now: i64): void {
|
|
521
|
+
sp.lossTime = -1
|
|
522
|
+
const delay: i64 = this.lossDelay()
|
|
523
|
+
const lostSendTime: i64 = now - delay
|
|
524
|
+
for (let k: i32 = 0; k < sp.count; k += 1) {
|
|
525
|
+
const s: i32 = sp.slot(k)
|
|
526
|
+
const pn: i64 = sp.pnAt(s)
|
|
527
|
+
if (pn > sp.largestAcked) {
|
|
528
|
+
break
|
|
529
|
+
}
|
|
530
|
+
if (sp.stateAt(s) !== QUIC_RECOVERY_SENT) {
|
|
531
|
+
continue
|
|
532
|
+
}
|
|
533
|
+
if (sp.timeAt(s) <= lostSendTime || sp.largestAcked >= pn + QUIC_RECOVERY_PACKET_THRESHOLD) {
|
|
534
|
+
sp.markLost(s)
|
|
535
|
+
} else {
|
|
536
|
+
const at: i64 = sp.timeAt(s) + delay
|
|
537
|
+
if (sp.lossTime < 0 || at < sp.lossTime) {
|
|
538
|
+
sp.lossTime = at
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* OnPacketsLost (B.8) for what `detectLoss` just declared lost: one
|
|
546
|
+
* congestion event for the newest of them, and persistent congestion
|
|
547
|
+
* when they span long enough.
|
|
548
|
+
*/
|
|
549
|
+
packetsLost(sp: QuicSentPackets, now: i64): void {
|
|
550
|
+
if (sp.lostCount === 0) {
|
|
551
|
+
return
|
|
552
|
+
}
|
|
553
|
+
let newest: i64 = -1
|
|
554
|
+
for (let k: i32 = 0; k < sp.lostCount; k += 1) {
|
|
555
|
+
newest = quicRecoveryMax(newest, sp.timeAt(sp.lostSlot(k)))
|
|
556
|
+
}
|
|
557
|
+
this.congestionEvent(newest, now)
|
|
558
|
+
if (this.persistentCongestion(sp)) {
|
|
559
|
+
this.congestionWindow = QUIC_RECOVERY_MINIMUM_WINDOW
|
|
560
|
+
this.recoveryStart = -1
|
|
561
|
+
this.persistentCongestions = this.persistentCongestions + 1
|
|
562
|
+
}
|
|
563
|
+
for (let k: i32 = 0; k < sp.lostCount; k += 1) {
|
|
564
|
+
sp.setState(sp.lostSlot(k), QUIC_RECOVERY_LOST)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Whether the packets just declared lost establish persistent congestion
|
|
570
|
+
* (§7.6.2): a run of lost ack-eliciting packets, all sent since the first
|
|
571
|
+
* RTT sample, with nothing acknowledged between the first and the last,
|
|
572
|
+
* spanning more than three probe timeouts with max_ack_delay, and holding
|
|
573
|
+
* at least one packet this call declared lost.
|
|
574
|
+
*/
|
|
575
|
+
persistentCongestion(sp: QuicSentPackets): boolean {
|
|
576
|
+
if (this.firstSampleTime < 0) {
|
|
577
|
+
return false
|
|
578
|
+
}
|
|
579
|
+
const span: i64 = (this.ptoBase() + this.maxAckDelay) * QUIC_RECOVERY_PERSISTENT_THRESHOLD
|
|
580
|
+
let first: i64 = -1
|
|
581
|
+
let fresh: boolean = false
|
|
582
|
+
for (let k: i32 = 0; k < sp.count; k += 1) {
|
|
583
|
+
const s: i32 = sp.slot(k)
|
|
584
|
+
const state: i32 = sp.stateAt(s)
|
|
585
|
+
const lost: boolean = state === QUIC_RECOVERY_LOST || state === QUIC_RECOVERY_LOST_NOW
|
|
586
|
+
if (state === QUIC_RECOVERY_ACKED || (lost && sp.timeAt(s) < this.firstSampleTime)) {
|
|
587
|
+
first = -1
|
|
588
|
+
fresh = false
|
|
589
|
+
continue
|
|
590
|
+
}
|
|
591
|
+
if (!lost) {
|
|
592
|
+
continue
|
|
593
|
+
}
|
|
594
|
+
if (first < 0) {
|
|
595
|
+
first = sp.timeAt(s)
|
|
596
|
+
}
|
|
597
|
+
fresh = fresh || state === QUIC_RECOVERY_LOST_NOW
|
|
598
|
+
if (fresh && sp.timeAt(s) - first > span) {
|
|
599
|
+
return true
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return false
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** Whether a packet sent at `sentTime` was sent during the current recovery period (B.4). */
|
|
606
|
+
inRecovery(sentTime: i64): boolean {
|
|
607
|
+
return this.recoveryStart >= 0 && sentTime <= this.recoveryStart
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** OnCongestionEvent (B.6): halve the window, once per recovery period. */
|
|
611
|
+
congestionEvent(sentTime: i64, now: i64): void {
|
|
612
|
+
if (this.inRecovery(sentTime)) {
|
|
613
|
+
return
|
|
614
|
+
}
|
|
615
|
+
this.recoveryStart = now
|
|
616
|
+
this.ssthresh = this.congestionWindow / 2
|
|
617
|
+
this.congestionWindow = quicRecoveryMax(this.ssthresh, QUIC_RECOVERY_MINIMUM_WINDOW)
|
|
618
|
+
this.congestionEvents = this.congestionEvents + 1
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* OnPacketAcked (B.5) for slot `s`: in slow start the window grows by the
|
|
623
|
+
* packet's size, in congestion avoidance by 1200 × size / window; not at
|
|
624
|
+
* all for a packet sent during recovery, or when `inUse` says the window
|
|
625
|
+
* was not being used (§7.8).
|
|
626
|
+
*/
|
|
627
|
+
packetAcked(sp: QuicSentPackets, s: i32, inUse: boolean): void {
|
|
628
|
+
if (!inUse || this.inRecovery(sp.timeAt(s))) {
|
|
629
|
+
return
|
|
630
|
+
}
|
|
631
|
+
const size: i64 = toI64(sp.sizeAt(s))
|
|
632
|
+
if (this.congestionWindow < this.ssthresh) {
|
|
633
|
+
this.congestionWindow = this.congestionWindow + size
|
|
634
|
+
} else {
|
|
635
|
+
this.congestionWindow =
|
|
636
|
+
this.congestionWindow + (QUIC_RECOVERY_DATAGRAM_SIZE * size) / this.congestionWindow
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* The probe timeout's period without backoff (§6.2.1): smoothed_rtt +
|
|
642
|
+
* max(4 × rttvar, kGranularity), plus the peer's max_ack_delay once the
|
|
643
|
+
* handshake is confirmed. It is also what a key update's old keys are kept
|
|
644
|
+
* for and what the idle timeout is at least three of.
|
|
645
|
+
*/
|
|
646
|
+
probeTimeout(): i64 {
|
|
647
|
+
const base: i64 = this.ptoBase()
|
|
648
|
+
return this.handshakeConfirmed ? base + this.maxAckDelay : base
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** smoothed_rtt + max(4 × rttvar, kGranularity): every probe timeout's period before max_ack_delay and the backoff. */
|
|
652
|
+
ptoBase(): i64 {
|
|
653
|
+
return this.smoothedRtt + quicRecoveryMax(4 * this.rttVar, QUIC_RECOVERY_GRANULARITY)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* GetPtoTimeAndSpace (A.8): when the probe timeout fires, the earliest
|
|
658
|
+
* over the spaces with an ack-eliciting packet in flight, with the backoff
|
|
659
|
+
* applied; the Application Data space counts only once the handshake is
|
|
660
|
+
* confirmed. Answers -1 with nothing in flight. `ptoSpace()` names the
|
|
661
|
+
* space it chose.
|
|
662
|
+
*/
|
|
663
|
+
ptoTime(): i64 {
|
|
664
|
+
const space: i32 = this.ptoSpace()
|
|
665
|
+
if (space < 0) {
|
|
666
|
+
return -1
|
|
667
|
+
}
|
|
668
|
+
return this.ptoTimeOf(space)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** The probe timeout of space `space`, from its last ack-eliciting packet, with the backoff. */
|
|
672
|
+
ptoTimeOf(space: i32): i64 {
|
|
673
|
+
const sp: QuicSentPackets | null = this.space(space)
|
|
674
|
+
if (sp === null || sp.lastAckEliciting < 0) {
|
|
675
|
+
return -1
|
|
676
|
+
}
|
|
677
|
+
const backoff: i64 = toI64(1) << toI64(this.ptoCount)
|
|
678
|
+
let duration: i64 = this.ptoBase()
|
|
679
|
+
if (space === QUIC_RECOVERY_APPLICATION) {
|
|
680
|
+
duration = duration + this.maxAckDelay
|
|
681
|
+
}
|
|
682
|
+
return sp.lastAckEliciting + duration * backoff
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/** The space whose probe timeout fires first, or -1 when no space may have one. */
|
|
686
|
+
ptoSpace(): i32 {
|
|
687
|
+
let best: i32 = -1
|
|
688
|
+
let bestTime: i64 = -1
|
|
689
|
+
for (let space: i32 = 0; space < toI32(this.spaces.length); space += 1) {
|
|
690
|
+
const sp: QuicSentPackets = this.spaces[space]
|
|
691
|
+
if (sp.inFlight === 0 || (space === QUIC_RECOVERY_APPLICATION && !this.handshakeConfirmed)) {
|
|
692
|
+
continue
|
|
693
|
+
}
|
|
694
|
+
const at: i64 = this.ptoTimeOf(space)
|
|
695
|
+
if (best < 0 || at < bestTime) {
|
|
696
|
+
best = space
|
|
697
|
+
bestTime = at
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return best
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/** The space with the earliest `lossTime`, or -1 when none is set. */
|
|
704
|
+
lossSpace(): i32 {
|
|
705
|
+
let best: i32 = -1
|
|
706
|
+
let bestTime: i64 = -1
|
|
707
|
+
for (let space: i32 = 0; space < toI32(this.spaces.length); space += 1) {
|
|
708
|
+
const at: i64 = this.spaces[space].lossTime
|
|
709
|
+
if (at >= 0 && (best < 0 || at < bestTime)) {
|
|
710
|
+
best = space
|
|
711
|
+
bestTime = at
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return best
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* SetLossDetectionTimer (A.8): when `onTimeout` is next due — the earliest
|
|
719
|
+
* time-threshold loss, else the probe timeout — or -1 for none. With
|
|
720
|
+
* `amplificationBlocked`, when the server may send nothing until the client
|
|
721
|
+
* does (RFC 9000 §8.1), there is no probe timeout (§6.2.2.1).
|
|
722
|
+
*/
|
|
723
|
+
deadline(amplificationBlocked: boolean): i64 {
|
|
724
|
+
const loss: i32 = this.lossSpace()
|
|
725
|
+
if (loss >= 0) {
|
|
726
|
+
return this.spaces[loss].lossTime
|
|
727
|
+
}
|
|
728
|
+
if (amplificationBlocked) {
|
|
729
|
+
return -1
|
|
730
|
+
}
|
|
731
|
+
return this.ptoTime()
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* OnLossDetectionTimeout (A.9), at `now`. When a time-threshold loss is
|
|
736
|
+
* due, the space's packets are checked again and the lost ones listed in
|
|
737
|
+
* its `lost`: `QUIC_RECOVERY_TIMEOUT_LOSS`. Otherwise, when the probe
|
|
738
|
+
* timeout is due, the backoff doubles and the caller is to send a probe in
|
|
739
|
+
* `timeoutSpace`: `QUIC_RECOVERY_TIMEOUT_PTO`. Before either is due, or
|
|
740
|
+
* when nothing is timed, `QUIC_RECOVERY_TIMEOUT_NONE`.
|
|
741
|
+
*/
|
|
742
|
+
onTimeout(now: i64, amplificationBlocked: boolean): i32 {
|
|
743
|
+
for (const sp of this.spaces) {
|
|
744
|
+
sp.compact()
|
|
745
|
+
sp.clearResults()
|
|
746
|
+
}
|
|
747
|
+
this.timeoutSpace = -1
|
|
748
|
+
const due: i64 = this.deadline(amplificationBlocked)
|
|
749
|
+
if (due < 0 || now < due) {
|
|
750
|
+
return QUIC_RECOVERY_TIMEOUT_NONE
|
|
751
|
+
}
|
|
752
|
+
const loss: i32 = this.lossSpace()
|
|
753
|
+
if (loss >= 0) {
|
|
754
|
+
const sp: QuicSentPackets = this.spaces[loss]
|
|
755
|
+
this.detectLoss(sp, now)
|
|
756
|
+
this.packetsLost(sp, now)
|
|
757
|
+
this.timeoutSpace = loss
|
|
758
|
+
return QUIC_RECOVERY_TIMEOUT_LOSS
|
|
759
|
+
}
|
|
760
|
+
this.timeoutSpace = this.ptoSpace()
|
|
761
|
+
if (this.ptoCount < QUIC_RECOVERY_MAX_BACKOFF) {
|
|
762
|
+
this.ptoCount = this.ptoCount + 1
|
|
763
|
+
}
|
|
764
|
+
return QUIC_RECOVERY_TIMEOUT_PTO
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Gives up the oldest packet still in flight in space `space`, taking it
|
|
769
|
+
* out of flight without a congestion event, so that a probe can be recorded
|
|
770
|
+
* when the ring is full. The caller has already queued what the space had
|
|
771
|
+
* in flight again, which is what a probe sends. Answers the slot, or -1
|
|
772
|
+
* when nothing is in flight there.
|
|
773
|
+
*/
|
|
774
|
+
evictOldest(space: i32): i32 {
|
|
775
|
+
const sp: QuicSentPackets | null = this.space(space)
|
|
776
|
+
if (sp === null) {
|
|
777
|
+
return -1
|
|
778
|
+
}
|
|
779
|
+
sp.compact()
|
|
780
|
+
for (let k: i32 = 0; k < sp.count; k += 1) {
|
|
781
|
+
const s: i32 = sp.slot(k)
|
|
782
|
+
if (sp.stateAt(s) === QUIC_RECOVERY_SENT) {
|
|
783
|
+
sp.settle(s, QUIC_RECOVERY_LOST)
|
|
784
|
+
return s
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return -1
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* OnPacketNumberSpaceDiscarded (A.11): the space's keys are gone, so its
|
|
792
|
+
* packets leave flight uncounted, its timers stop, and the backoff resets.
|
|
793
|
+
*/
|
|
794
|
+
discardSpace(space: i32): void {
|
|
795
|
+
const sp: QuicSentPackets | null = this.space(space)
|
|
796
|
+
if (sp === null) {
|
|
797
|
+
return
|
|
798
|
+
}
|
|
799
|
+
sp.clear()
|
|
800
|
+
this.ptoCount = 0
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Tops the pacer up to `now` (§7.7): it earns 5/4 of the window per
|
|
805
|
+
* smoothed RTT, and holds at most the initial window, which is the largest
|
|
806
|
+
* burst it allows.
|
|
807
|
+
*/
|
|
808
|
+
pacerRefill(now: i64): void {
|
|
809
|
+
if (this.pacerTime < 0 || now < this.pacerTime) {
|
|
810
|
+
this.pacerTime = now
|
|
811
|
+
return
|
|
812
|
+
}
|
|
813
|
+
// A minute is far past what fills the budget, and keeps the product in range.
|
|
814
|
+
let elapsed: i64 = now - this.pacerTime
|
|
815
|
+
if (elapsed > 60000) {
|
|
816
|
+
elapsed = 60000
|
|
817
|
+
}
|
|
818
|
+
const rtt: i64 = quicRecoveryMax(this.smoothedRtt, QUIC_RECOVERY_GRANULARITY)
|
|
819
|
+
const earned: i64 = (elapsed * this.congestionWindow * 5) / (rtt * 4)
|
|
820
|
+
this.pacerBudget = this.pacerBudget + earned
|
|
821
|
+
if (this.pacerBudget > QUIC_RECOVERY_INITIAL_WINDOW) {
|
|
822
|
+
this.pacerBudget = QUIC_RECOVERY_INITIAL_WINDOW
|
|
823
|
+
}
|
|
824
|
+
this.pacerTime = now
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* How many milliseconds after `now` the pacer lets a datagram of `size`
|
|
829
|
+
* bytes go: 0 when it may go now, else at least 1. The rate is 5/4 of the
|
|
830
|
+
* window per smoothed RTT (§7.7's N of 1.25).
|
|
831
|
+
*/
|
|
832
|
+
pacerDelay(now: i64, size: i32): i64 {
|
|
833
|
+
this.pacerRefill(now)
|
|
834
|
+
const need: i64 = toI64(size) - this.pacerBudget
|
|
835
|
+
if (need <= 0) {
|
|
836
|
+
return 0
|
|
837
|
+
}
|
|
838
|
+
const rtt: i64 = quicRecoveryMax(this.smoothedRtt, QUIC_RECOVERY_GRANULARITY)
|
|
839
|
+
const rate: i64 = quicRecoveryMax(this.congestionWindow * 5, 1)
|
|
840
|
+
const wait: i64 = (need * rtt * 4 + rate - 1) / rate
|
|
841
|
+
return quicRecoveryMax(wait, 1)
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/** Charges the pacer for a datagram of `size` bytes sent at `now`. */
|
|
845
|
+
onPaced(now: i64, size: i32): void {
|
|
846
|
+
this.pacerRefill(now)
|
|
847
|
+
this.pacerBudget = this.pacerBudget - toI64(size)
|
|
848
|
+
}
|
|
849
|
+
}
|