GeezerVison is showing github.com — stream, quic: update iterable streams backpressure · nodejs/node@125c19d
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: benchmark/streams/iter-throughput-share.js +2-2Lines changed: 2 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -5,7 +5,7 @@ const common = require('../common.js'); | 5 | 5 | const bench = common.createBenchmark(main, { | 6 | 6 | consumers: [2, 8, 32], | 7 | 7 | batches: [1e4], | 8 | | - backpressure: ['block'], | | 8 | + backpressure: ['unbounded'], | 9 | 9 | n: [5], | 10 | 10 | }, { | 11 | 11 | flags: ['--experimental-stream-iter'], | @@ -24,7 +24,7 @@ async function main({ consumers, batches, backpressure, n }) { | 24 | 24 |
| 25 | 25 | bench.start(); | 26 | 26 | for (let i = 0; i < n; i++) { | 27 | | - const shared = share(source(), { highWaterMark: 64, backpressure }); | | 27 | + const shared = share(source(), { budget: 65536, backpressure }); | 28 | 28 | const readers = Array.from({ length: consumers }, () => array(shared.pull())); | 29 | 29 | await Promise.all(readers); | 30 | 30 | } | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/quic.md +5-4Lines changed: 5 additions & 4 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1276,7 +1276,7 @@ added: v23.8.0 | 1276 | 1276 | interleaved with data from other streams of the same priority level. | 1277 | 1277 | When `false`, the stream should be completed before same-priority peers. | 1278 | 1278 | **Default:** `false`. | 1279 | | - * `highWaterMark` {number} The maximum number of bytes that the writer | | 1279 | + * `budget` {number} The maximum number of bytes that the writer | 1280 | 1280 | will buffer before `writeSync()` returns `false`. When the buffered | 1281 | 1281 | data exceeds this limit, the caller should wait for drain before | 1282 | 1282 | writing more. **Default:** `65536` (64 KB). | @@ -1317,7 +1317,7 @@ added: v23.8.0 | 1317 | 1317 | interleaved with data from other streams of the same priority level. | 1318 | 1318 | When `false`, the stream should be completed before same-priority peers. | 1319 | 1319 | **Default:** `false`. | 1320 | | - * `highWaterMark` {number} The maximum number of bytes that the writer | | 1320 | + * `budget` {number} The maximum number of bytes that the writer | 1321 | 1321 | will buffer before `writeSync()` returns `false`. When the buffered | 1322 | 1322 | data exceeds this limit, the caller should wait for drain before | 1323 | 1323 | writing more. **Default:** `65536` (64 KB). | @@ -1924,7 +1924,7 @@ added: v23.8.0 | 1924 | 1924 | The directionality of the stream, or `null` if the stream has been destroyed | 1925 | 1925 | or is still pending. Read only. | 1926 | 1926 |
| 1927 | | -### `stream.highWaterMark` | | 1927 | +### `stream.budget` | 1928 | 1928 |
| 1929 | 1929 | <!-- YAML | 1930 | 1930 | added: REPLACEME | @@ -2236,7 +2236,8 @@ The Writer has the following methods: | 2236 | 2236 | the QUIC transport-layer `INTERNAL_ERROR` (`0x1`) for raw QUIC). | 2237 | 2237 | See [`stream.destroy()`][] for a full-stream abort that also resets | 2238 | 2238 | the readable side via `STOP_SENDING`. | 2239 | | -* `desiredSize` — Available capacity in bytes, or `null` if closed/errored. | | 2239 | +* `canWrite` — `true` if writes will be accepted, `false` if at capacity, | | 2240 | + or `null` if closed/errored. | 2240 | 2241 |
| 2241 | 2242 | The bytes from each `writeSync()` / `writevSync()` / `write()` / `writev()` | 2242 | 2243 | input chunk are copied into an internal buffer, so the caller's source | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/stream_iter.md +3-3Lines changed: 3 additions & 3 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -223,7 +223,7 @@ How each policy uses these buffers: | 223 | 223 |
| 224 | 224 | | Policy | Buffer limit | Pending writes limit | | 225 | 225 | | --------------- | ------------ | -------------------- | | 226 | | -| `'strict'` | `budget` | `budget` | | | 226 | +| `'strict'` | `budget` | 1 | | 227 | 227 | | `'unbounded'` | `budget` | Unbounded | | 228 | 228 | | `'drop-oldest'` | `budget` | N/A (never waits) | | 229 | 229 | | `'drop-newest'` | `budget` | N/A (never waits) | | @@ -232,8 +232,8 @@ How each policy uses these buffers: | 232 | 232 |
| 233 | 233 | Strict mode catches "fire-and-forget" patterns where the producer calls | 234 | 234 | `write()` without awaiting, which would cause unbounded memory growth. | 235 | | -It limits both the buffer and the pending writes queue to | 236 | | -`budget` bytes. | | 235 | +It limits the buffer to `budget` bytes and the pending writes queue | | 236 | +to a single entry. | 237 | 237 |
| 238 | 238 | If you properly await each write, you can only ever have one pending | 239 | 239 | write at a time (yours), so you never hit the pending writes limit. | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/quic/quic.js +25-25Lines changed: 25 additions & 25 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -297,8 +297,8 @@ const endpointRegistry = new SafeSet(); | 297 | 297 | * (e.g. HTTP/3). | 298 | 298 | * @property {'high'|'default'|'low'} [priority] The priority level of the stream. | 299 | 299 | * @property {boolean} [incremental] Whether to interleave data with same-priority streams. | 300 | | - * @property {number} [highWaterMark] The high water mark for write | 301 | | - * backpressure, in bytes. **Default:** `65536`. | | 300 | + * @property {number} [budget] The byte budget for write backpressure. | | 301 | + * **Default:** `65536`. | 302 | 302 | * @property {OnHeadersCallback} [onheaders] Callback for incoming initial headers | 303 | 303 | * @property {OnTrailersCallback} [ontrailers] Callback for incoming trailing headers | 304 | 304 | * @property {OnInfoCallback} [oninfo] Callback for informational (1xx) headers | @@ -1325,7 +1325,7 @@ function applyCallbacks(session, cbs) { | 1325 | 1325 | * @param {QuicStream} stream The JS stream object | 1326 | 1326 | * @param {any} body The body source | 1327 | 1327 | */ | 1328 | | -const kDefaultHighWaterMark = 65536; | | 1328 | +const kDefaultBudget = 65536; | 1329 | 1329 | const kDefaultMaxPendingDatagrams = 128; | 1330 | 1330 |
| 1331 | 1331 | function configureOutbound(handle, stream, body) { | @@ -1405,20 +1405,20 @@ function configureOutbound(handle, stream, body) { | 1405 | 1405 | ); | 1406 | 1406 | } | 1407 | 1407 |
| 1408 | | -// Sets the high water mark and initial writeDesiredSize for a streaming | | 1408 | +// Sets the budget and initial writeDesiredSize for a streaming | 1409 | 1409 | // outbound source. Called after handle.initStreamingSource() for both | 1410 | 1410 | // body-source and writer paths. One-shot body sources (string, Uint8Array, | 1411 | 1411 | // Blob, FileHandle, etc.) do not use this -- they go through attachSource | 1412 | 1412 | // and are not subject to backpressure. | 1413 | 1413 | function initStreamingBackpressure(stream) { | 1414 | 1414 | const state = getQuicStreamState(stream); | 1415 | 1415 | // Only set defaults if the user hasn't already configured them | 1416 | | - // (e.g., via createBidirectionalStream({ highWaterMark: N })). | 1417 | | - if (state.highWaterMark === 0) { | 1418 | | - state.highWaterMark = kDefaultHighWaterMark; | | 1416 | + // (e.g., via createBidirectionalStream({ budget: N })). | | 1417 | + if (state.budget === 0) { | | 1418 | + state.budget = kDefaultBudget; | 1419 | 1419 | } | 1420 | 1420 | if (state.writeDesiredSize === 0) { | 1421 | | - state.writeDesiredSize = state.highWaterMark; | | 1421 | + state.writeDesiredSize = state.budget; | 1422 | 1422 | } | 1423 | 1423 | } | 1424 | 1424 |
| @@ -1699,23 +1699,23 @@ class QuicStream { | 1699 | 1699 | } | 1700 | 1700 |
| 1701 | 1701 | /** | 1702 | | - * The high water mark for write backpressure. When the total queued | | 1702 | + * The byte budget for write backpressure. When the total queued | 1703 | 1703 | * outbound bytes exceeds this value, writeSync returns false and | 1704 | | - * desiredSize drops to 0. Default is 65536 (64KB). | | 1704 | + * canWrite returns false. Default is 65536 (64KB). | 1705 | 1705 | * @type {number} | 1706 | 1706 | */ | 1707 | | - get highWaterMark() { | | 1707 | + get budget() { | 1708 | 1708 | assertIsQuicStream(this); | 1709 | | - return this.#inner.state.highWaterMark; | | 1709 | + return this.#inner.state.budget; | 1710 | 1710 | } | 1711 | 1711 |
| 1712 | | - set highWaterMark(val) { | | 1712 | + set budget(val) { | 1713 | 1713 | assertIsQuicStream(this); | 1714 | | - validateInteger(val, 'highWaterMark', 0, 0xFFFFFFFF); | | 1714 | + validateInteger(val, 'budget', 0, 0xFFFFFFFF); | 1715 | 1715 | const inner = this.#inner; | 1716 | | - inner.state.highWaterMark = val; | | 1716 | + inner.state.budget = val; | 1717 | 1717 | // If writeDesiredSize hasn't been set yet (still 0 from initialization), | 1718 | | - // initialize it to the highWaterMark so the first write can proceed. | | 1718 | + // initialize it to the budget so the first write can proceed. | 1719 | 1719 | if (inner.state.writeDesiredSize === 0 && val > 0) { | 1720 | 1720 | inner.state.writeDesiredSize = val; | 1721 | 1721 | } | @@ -2163,8 +2163,8 @@ class QuicStream { | 2163 | 2163 | // will accept the data into the DataQueue and | 2164 | 2164 | // UpdateWriteDesiredSize() will drop writeDesiredSize toward 0, | 2165 | 2165 | // at which point the standard drain mechanism takes over. | 2166 | | - // This follows the Web Streams model where writes beyond the HWM | 2167 | | - // succeed and backpressure applies to *subsequent* writes. | | 2166 | + // This follows the iter-streams model where writes beyond the | | 2167 | + // budget succeed and backpressure applies to *subsequent* writes. | 2168 | 2168 | if (stream.#inner.state.writeDesiredSize === 0) return false; | 2169 | 2169 | const result = handle.write([chunk]); | 2170 | 2170 | if (result === undefined) return false; | @@ -2323,9 +2323,9 @@ class QuicStream { | 2323 | 2323 |
| 2324 | 2324 | const writer = { | 2325 | 2325 | __proto__: null, | 2326 | | - get desiredSize() { | | 2326 | + get canWrite() { | 2327 | 2327 | if (closed || errored || stream.#inner.state.writeEnded) return null; | 2328 | | - return stream.#inner.state.writeDesiredSize; | | 2328 | + return stream.#inner.state.writeDesiredSize > 0; | 2329 | 2329 | }, | 2330 | 2330 | writeSync, | 2331 | 2331 | write, | @@ -3254,7 +3254,7 @@ class QuicSession { | 3254 | 3254 | body, | 3255 | 3255 | priority = 'default', | 3256 | 3256 | incremental = false, | 3257 | | - highWaterMark = kDefaultHighWaterMark, | | 3257 | + budget = kDefaultBudget, | 3258 | 3258 | headers, | 3259 | 3259 | onheaders, | 3260 | 3260 | ontrailers, | @@ -3290,8 +3290,8 @@ class QuicSession { | 3290 | 3290 | stream[kAttachFileHandle](body); | 3291 | 3291 | } | 3292 | 3292 |
| 3293 | | - // Set the high water mark for backpressure. | 3294 | | - stream.highWaterMark = highWaterMark; | | 3293 | + // Set the byte budget for backpressure. | | 3294 | + stream.budget = budget; | 3295 | 3295 |
| 3296 | 3296 | // Set stream callbacks before sending headers to avoid missing events. | 3297 | 3297 | if (onheaders) stream.onheaders = onheaders; | @@ -4047,8 +4047,8 @@ class QuicSession { | 4047 | 4047 | const stream = new QuicStream(kPrivateConstructor, handle, this, direction, | 4048 | 4048 | false /* isLocal */); | 4049 | 4049 |
| 4050 | | - // Set the default high water mark for received streams. | 4051 | | - stream.highWaterMark = kDefaultHighWaterMark; | | 4050 | + // Set the default byte budget for received streams. | | 4051 | + stream.budget = kDefaultBudget; | 4052 | 4052 |
| 4053 | 4053 | // A new stream was received. If we don't have an onstream callback, then | 4054 | 4054 | // there's nothing we can do about it. Destroy the stream in this case. | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/quic/state.js +9-9Lines changed: 9 additions & 9 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -104,7 +104,7 @@ const { | 104 | 104 | IDX_STATE_STREAM_WANTS_TRAILERS, | 105 | 105 | IDX_STATE_STREAM_RECEIVED_EARLY_DATA, | 106 | 106 | IDX_STATE_STREAM_WRITE_DESIRED_SIZE, | 107 | | - IDX_STATE_STREAM_HIGH_WATER_MARK, | | 107 | + IDX_STATE_STREAM_BUDGET, | 108 | 108 | IDX_STATE_STREAM_RESET_CODE, | 109 | 109 | } = internalBinding('quic'); | 110 | 110 |
| @@ -871,18 +871,18 @@ class QuicStreamState { | 871 | 871 | } | 872 | 872 |
| 873 | 873 | /** @type {number} */ | 874 | | - get highWaterMark() { | | 874 | + get budget() { | 875 | 875 | const handle = this.#handle; | 876 | 876 | if (handle === undefined) return undefined; | 877 | 877 | return DataViewPrototypeGetUint32( | 878 | | - handle, this.#offset + IDX_STATE_STREAM_HIGH_WATER_MARK, kIsLittleEndian); | | 878 | + handle, this.#offset + IDX_STATE_STREAM_BUDGET, kIsLittleEndian); | 879 | 879 | } | 880 | 880 |
| 881 | | - set highWaterMark(val) { | | 881 | + set budget(val) { | 882 | 882 | const handle = this.#handle; | 883 | 883 | if (handle === undefined) return; | 884 | 884 | DataViewPrototypeSetUint32( | 885 | | - handle, this.#offset + IDX_STATE_STREAM_HIGH_WATER_MARK, val, kIsLittleEndian); | | 885 | + handle, this.#offset + IDX_STATE_STREAM_BUDGET, val, kIsLittleEndian); | 886 | 886 | } | 887 | 887 |
| 888 | 888 | toString() { | @@ -908,7 +908,7 @@ class QuicStreamState { | 908 | 908 | early, | 909 | 909 | resetCode, | 910 | 910 | writeDesiredSize, | 911 | | - highWaterMark, | | 911 | + budget, | 912 | 912 | } = this; | 913 | 913 | return { | 914 | 914 | __proto__: null, | @@ -928,7 +928,7 @@ class QuicStreamState { | 928 | 928 | early, | 929 | 929 | resetCode, | 930 | 930 | writeDesiredSize, | 931 | | - highWaterMark, | | 931 | + budget, | 932 | 932 | }; | 933 | 933 | } | 934 | 934 |
| @@ -965,7 +965,7 @@ class QuicStreamState { | 965 | 965 | early, | 966 | 966 | resetCode, | 967 | 967 | writeDesiredSize, | 968 | | - highWaterMark, | | 968 | + budget, | 969 | 969 | } = this; | 970 | 970 |
| 971 | 971 | return `QuicStreamState ${inspect({ | @@ -985,7 +985,7 @@ class QuicStreamState { | 985 | 985 | early, | 986 | 986 | resetCode, | 987 | 987 | writeDesiredSize, | 988 | | - highWaterMark, | | 988 | + budget, | 989 | 989 | }, opts)}`; | 990 | 990 | } | 991 | 991 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/streams/iter/broadcast.js +1-2Lines changed: 1 addition & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -55,7 +55,6 @@ const { | 55 | 55 | const { | 56 | 56 | kMultiConsumerDefaultBudget, | 57 | 57 | kResolvedPromise, | 58 | | - clampBudget, | 59 | 58 | convertChunks, | 60 | 59 | getWriterSignal, | 61 | 60 | getMinCursor, | @@ -779,7 +778,7 @@ function broadcast(options = { __proto__: null }) { | 779 | 778 |
| 780 | 779 | const opts = { | 781 | 780 | __proto__: null, | 782 | | - budget: clampBudget(budget), | | 781 | + budget, | 783 | 782 | backpressure, | 784 | 783 | signal, | 785 | 784 | }; | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/streams/iter/push.js +16-3Lines changed: 16 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -37,7 +37,6 @@ const { | 37 | 37 | const { | 38 | 38 | kPushDefaultBudget, | 39 | 39 | kResolvedPromise, | 40 | | - clampBudget, | 41 | 40 | onSignalAbort, | 42 | 41 | toUint8Array, | 43 | 42 | convertChunks, | @@ -99,7 +98,7 @@ class PushQueue { | 99 | 98 | if (signal !== undefined) { | 100 | 99 | validateAbortSignal(signal, 'options.signal'); | 101 | 100 | } | 102 | | - this.#budget = clampBudget(budget); | | 101 | + this.#budget = budget; | 103 | 102 | this.#backpressure = backpressure; | 104 | 103 | this.#signal = signal; | 105 | 104 | this.#abortHandler = undefined; | @@ -127,7 +126,12 @@ class PushQueue { | 127 | 126 | if (this.#writerState !== 'open' || this.#consumerState !== 'active') { | 128 | 127 | return null; | 129 | 128 | } | 130 | | - return this.#bufferedBytes < this.#budget; | | 129 | + if ((this.#backpressure === 'strict' || | | 130 | + this.#backpressure === 'unbounded') && | | 131 | + this.#bufferedBytes >= this.#budget) { | | 132 | + return false; | | 133 | + } | | 134 | + return true; | 131 | 135 | } | 132 | 136 |
| 133 | 137 | /** | @@ -156,6 +160,10 @@ class PushQueue { | 156 | 160 |
| 157 | 161 | const batchSize = this.#batchByteSize(chunks); | 158 | 162 |
| | 163 | + // Skip empty chunks -- zero-byte writes would accumulate infinitely | | 164 | + // without ever triggering backpressure under a byte-budget model. | | 165 | + if (batchSize === 0) return true; | | 166 | + | 159 | 167 | if (this.#bufferedBytes >= this.#budget) { | 160 | 168 | switch (this.#backpressure) { | 161 | 169 | case 'strict': | @@ -181,6 +189,11 @@ class PushQueue { | 181 | 189 | this.#bytesWritten += batchSize; | 182 | 190 |
| 183 | 191 | this.#resolvePendingReads(); | | 192 | + // After drop-oldest, evicting a large chunk may bring us under budget. | | 193 | + // Resolve pending drains so writers waiting on backpressure can proceed. | | 194 | + if (this.#bufferedBytes < this.#budget) { | | 195 | + this.#resolvePendingDrains(true); | | 196 | + } | 184 | 197 | return true; | 185 | 198 | } | 186 | 199 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/streams/iter/share.js +2-3Lines changed: 2 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -36,7 +36,6 @@ const { | 36 | 36 |
| 37 | 37 | const { | 38 | 38 | kMultiConsumerDefaultBudget, | 39 | | - clampBudget, | 40 | 39 | getMinCursor, | 41 | 40 | hasProtocol, | 42 | 41 | onSignalAbort, | @@ -696,7 +695,7 @@ function share(source, options = { __proto__: null }) { | 696 | 695 |
| 697 | 696 | const opts = { | 698 | 697 | __proto__: null, | 699 | | - budget: clampBudget(budget), | | 698 | + budget, | 700 | 699 | backpressure, | 701 | 700 | signal, | 702 | 701 | }; | @@ -723,7 +722,7 @@ function shareSync(source, options = { __proto__: null }) { | 723 | 722 |
| 724 | 723 | const opts = { | 725 | 724 | __proto__: null, | 726 | | - budget: clampBudget(budget), | | 725 | + budget, | 727 | 726 | backpressure, | 728 | 727 | }; | 729 | 728 |
| |
|