Skip to content

stream: update iterable streams to use budget backpressure#64464

Open
jasnell wants to merge 2 commits into
nodejs:mainfrom
jasnell:jasnell/iter-streams-backpressure-new
Open

stream: update iterable streams to use budget backpressure#64464
jasnell wants to merge 2 commits into
nodejs:mainfrom
jasnell:jasnell/iter-streams-backpressure-new

Conversation

@jasnell

@jasnell jasnell commented Jul 13, 2026

Copy link
Copy Markdown
Member

WinterTC55/iter-streams#23 updated the backpressure model in the iterable streams specification to use a byte-budget model. This PR updates the implementation to match.

Signed-off-by: James M Snell jasnell@gmail.com
Assisted-by: Opencode/Opus

jasnell added 2 commits July 12, 2026 16:41
Updates the implementation to use the updated backpressure
model that landed here: WinterTC55/iter-streams#23

Signed-off-by: James M Snell <jasnell@gmail.com>
Assisted-by: Opencode/Opus
Signed-off-by: James M Snell <jasnell@gmail.com>
Assisted-by: Opencode/Opus
@jasnell jasnell requested review from mcollina and trivikr July 13, 2026 00:27
@jasnell jasnell added stream Issues and PRs related to the stream subsystem. experimental Issues and PRs related to experimental features. labels Jul 13, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/quic
  • @nodejs/streams

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Jul 13, 2026
@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.16279% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.24%. Comparing base (b4c7be3) to head (939e46e).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/streams/iter/share.js 77.41% 14 Missing ⚠️
lib/internal/streams/iter/broadcast.js 93.47% 3 Missing ⚠️
lib/internal/streams/iter/classic.js 66.66% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #64464      +/-   ##
==========================================
+ Coverage   90.23%   90.24%   +0.01%     
==========================================
  Files         741      741              
  Lines      241604   241652      +48     
  Branches    45520    45528       +8     
==========================================
+ Hits       218010   218079      +69     
+ Misses      15120    15101      -19     
+ Partials     8474     8472       -2     
Files with missing lines Coverage Δ
lib/internal/quic/quic.js 100.00% <100.00%> (ø)
lib/internal/quic/state.js 100.00% <100.00%> (ø)
lib/internal/streams/iter/duplex.js 96.35% <100.00%> (ø)
lib/internal/streams/iter/pull.js 84.12% <100.00%> (+0.25%) ⬆️
lib/internal/streams/iter/push.js 90.98% <100.00%> (+0.13%) ⬆️
lib/internal/streams/iter/utils.js 98.21% <100.00%> (ø)
lib/internal/streams/iter/classic.js 89.25% <66.66%> (-0.02%) ⬇️
lib/internal/streams/iter/broadcast.js 86.22% <93.47%> (+0.26%) ⬆️
lib/internal/streams/iter/share.js 85.33% <77.41%> (+0.60%) ⬆️

... and 29 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread doc/api/stream_iter.md
| `'drop-newest'` | `highWaterMark` | N/A (never waits) |
| Policy | Buffer limit | Pending writes limit |
| --------------- | ------------ | -------------------- |
| `'strict'` | `budget` | `budget` |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the pending writes limit is now 1 - docs need updating here

this.#bufferedBytes += batchSize;
this.#bytesWritten += batchSize;

this.#resolvePendingReads();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs to potentially resolve pending drains:

Suggested change
this.#resolvePendingReads();
this.#resolvePendingReads();
if (this.#bufferedBytes < this.#budget) {
this.#resolvePendingDrains(true);
}

Since we're dropping slot-wise, with drop-oldest, a write can bring us back under the limit (e.g. a 1KB write that makes us drop an 100KB chunk). If we don't resolve drains here, backpressure deadlocks.

if (this.#consumerState !== 'active') return false;

if (this.#slots.length >= this.#highWaterMark) {
const batchSize = this.#batchByteSize(chunks);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We currently don't skip empty chunks here. Should we? Changes behaviour a bit, but otherwise bytewise budgets mean you can push infinite empty chunks, and you get weird behaviour like zero-byte writes waiting on backpressure.

Would be easy to check and drop here if we think that's the right thing to do.

const opts = {
__proto__: null,
highWaterMark: clampHWM(highWaterMark),
budget: clampBudget(budget),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everywhere we run clampBudget we run validateInteger(budget, 'options.budget', 16384) directly beforehand and AFAICT they do exactly the same thing.

return null;
}
return MathMax(0, this.#highWaterMark - this.#slots.length);
return this.#bufferedBytes < this.#budget;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is false when full on drop-*, while BroadcastWriter streams canWrite returns true in that case (kCanWrite logic differs according to backpressure policy). They should match I think.

function clampHWM(value) {
return MathMax(1, MathMin(NumberMAX_SAFE_INTEGER, value));
function clampBudget(value) {
return MathMax(16384, MathMin(NumberMAX_SAFE_INTEGER, value));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why (here or validate) the hard limit of 16KB as a lower bound? At a glance seems arbitrary - I imagine smaller is less efficient, but if people want to do weird things/have weird constraints do we need to block them? I don't feel strongly but I'd be interested if there's a specific reason.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual limit we end up with (here and in the spec) needs to be fully benchmarked and validated. Went with something that I knew would prompt the question ;-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

experimental Issues and PRs related to experimental features. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. stream Issues and PRs related to the stream subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants