Skip to content

perf(net): drop a finished stream's reader instead of releasing its lock - #6479

Merged
kixelated merged 4 commits into
mainfrom
quest/m1/release-perf/js-frames
Oct 23, 2026
Merged

kixelated merged 4 commits into
mainfrom
quest/m1/release-perf/js-frames

Conversation

@kixelated

@kixelated kixelated commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Finishes quest/m1/release-perf/js-frames.md.

Summary

The remaining js/net frame gap against release came from one line. Since #4857, Reader.#fill calls releaseLock() once a stream reports done. On a stream that has already ended, releasing the lock replaces the reader's closed promise with a new rejected one. That builds a TypeError with a stack for every group stream. In Chromium this costs about 5 us per stream. Spread over the bench's 50 frames per group, it accounted for the 1.2 to 1.5x on batched lite rows and most of the IETF gap.

The reader is now dropped, not released. #4857 needed the drop: a later stop() still sends nothing and builds no error. Nothing reads the stream again after FIN, so the lock never needs releasing.

A regression test reads 32 streams to FIN and asserts no TypeError or Error is retained. It fails with 64 on the old code. frames.ts already shows the cost in Chromium, so no benchmark changes.

The quest file is deleted. The epic records the result, and its final step now re-runs the JS frame path in Chromium. That step replaces this quest's "re-run after the other children land" item: both siblings are Rust-only.

Attribution

All runs used headless Chromium from the Nix shell, on a shared 24-core host with load average 19 to 35.

The whole-run harness (bench/browser/micro.ts, one variant per page, three rounds) gave A/A ratios from 0.36 to 1.55 here, too wide to rank anything. So variants were bundled into one page and interleaved per row, with the starting variant rotated each repetition. The tables report the median of per-repetition ratios, with the IQR in brackets.

Component split at main (21 reps, ns per frame):

Bench release main main/release A/A
decode, 1 frame per stream 2050 6375 2.63 1.04
decode, 50 x 16 B frames per stream 125 195 1.52 1.04
decode, 1000 x 16 B frames per stream 150 150 1.00 -
group write/read, 50 frames, one wakeup 190 200 1.03 1.05
group write/read, 50 frames, a wakeup each 200 225 1.12 1.08

So the decode path was at parity per frame and the gap was per stream. Removing releaseLock alone brought decode with 1 frame per stream from 8225 to 2500 ns against release's 2575.

frames.ts, release vs this branch (lite 15 reps at 12% of the frames, IETF 15 reps at 25%):

Rows this branch / release A/A (aa / release)
lite, 13 rows 0.99 to 1.12 0.94 to 1.19
IETF, 13 rows 0.82 to 1.11 0.82 to 1.09

Before the fix, the same harness put main at 1.23 to 1.48 on batched lite rows and 1.13 to 1.57 on IETF rows.

Tried and dropped

Group-path changes were tried and measured, then dropped: skipping the compression WeakMap lookup per write, and skipping the per-read frame copy that #6230 added for plain frames. Neither moved the group benches outside noise. The group path's 3 to 12% sits on an A/A of 3 to 8%, which this host can't separate.

Public API and wire impact

None.

Checks

  • just check: pass.
  • bun test src/stream.test.ts: 66 pass. The new test fails on the old code.
  • bench/browser/micro.ts --bench frames (whole-run harness): two of three rounds finished. In round 3 the existing maxBatchedRatio assertion tripped on a 136 us/frame host stall in an IETF row. That assertion predates this PR.

Decisions

Decided 2026-10-23 (maintainer): browser parity accepted on this evidence. Every frames.ts row is within the A/A spread of release, and the group path's 3 to 12% sits on an A/A of 3 to 8%. The epic records this, and its final step re-runs the JS frame path in Chromium.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 2 commits October 22, 2026 20:56
Releasing the reader lock of a stream that already ended replaces its
closed promise with a rejected one, which builds a TypeError for every
group stream. In Chromium that cost about 5 us per stream, which put the
js/net frame benchmark 1.2 to 1.5x behind release. Dropping the reference
is enough to keep a later stop from cancelling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title JS frames perf(net): drop a finished stream's reader instead of releasing its lock Oct 23, 2026
@kixelated

Copy link
Copy Markdown
Collaborator Author

Outcome: I traced the remaining browser gap to Reader.#fill calling releaseLock() after FIN. That builds a TypeError per group stream, about 5 us in Chromium. This PR drops the reader instead. After the change, every frames.ts row is within the A/A spread of release in paired same-page runs. The PR stays a draft until a maintainer settles the one decision in the body: whether to call parity and finish the quest.

(Written by Claude Opus 5.5)

Record the maintainer's decision to accept browser parity in the epic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review October 23, 2026 05:14
@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 11 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e0c3190f-ddc7-4207-990f-c02f901c8ce2
📥 Commits

Reviewing files that changed from the base of the PR and between 69b0538 and 11cb614.

📒 Files selected for processing: 4
  • js/net/src/stream.test.ts
  • js/net/src/stream.ts
  • quest/m1/release-perf/README.md
  • quest/m1/release-perf/js-frames.md
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-10-23T06:18:31.099922Z 11cb614 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9fce081e1b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread js/net/src/stream.ts
Reader kept its ReadableStream only to call getReader(), so after FIN the
still-locked stream held the dropped reader alive for as long as the Reader
was reachable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

@codex review

(Written by Claude Opus 5.5)

@kixelated

Copy link
Copy Markdown
Collaborator Author

Merging. Summary:

  • Change: Reader.#fill drops a finished stream's reader instead of calling releaseLock(), which built a TypeError per group stream (about 5 us each in Chromium). Regression test covers it; the quest file is deleted and the epic records the result.
  • Decision: browser parity accepted by the maintainer on the measured evidence (recorded in the body).
  • Reviews: Codex reviewed 9fce081 with one finding (a reachable Reader kept the locked stream and dropped reader alive via #stream). Fixed in 11cb614 by removing the unused field, a 3-line deletion. The Codex re-review of 11cb614 has been running for about 10 hours after one @codex review nudge, so this merges under the trivial-change-since-last-review exception.
  • CI: all 9 checks pass on 11cb614. quest check --base origin/main passes.
  • Public API and wire impact: none.

(Written by Claude Opus 5.5)

@kixelated
kixelated added this pull request to the merge queue Oct 23, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

Merged via the queue into main with commit 2bbdf2c Oct 23, 2026
9 checks passed
@kixelated
kixelated deleted the quest/m1/release-perf/js-frames branch October 23, 2026 17:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant