Skip to content

perf(socket): release the batch buffer a burst grew once the burst ends - #1246

Merged
jlucaso1 merged 3 commits into
mainfrom
perf/per-session-marginal-noise-buffer
Aug 8, 2026
Merged

perf(socket): release the batch buffer a burst grew once the burst ends#1246
jlucaso1 merged 3 commits into
mainfrom
perf/per-session-marginal-noise-buffer

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Target 4 of the per-session marginal batch, and the largest of the five: 52 KiB per session, paid by any session that sends one large stanza.

#1235 left this one out of the report because the batch buffer is a local inside a spawned task and nothing can read it from outside. That is still true, but it turns out not to matter — the socket's own mock-transport scaffolding drives the sender end to end, so residency is measurable from RssAnon without a live WhatsApp session. Median over 16 sockets, release:

traffic KiB resident per socket
small frames only 8.2
one 60 KiB frame, then small frames 60.2

One media-sized stanza raises the socket's floor by 52 KiB and it never comes back down for the life of the connection.

Design

out_buf.split() hands the written bytes to the transport and leaves out_buf empty — but both halves share the allocation, so out_buf keeps a handle to it, and once the Bytes is dropped the next reserve reclaims the whole thing. That is what makes the batching zero-copy, and it is also what makes the high-water mark permanent: the buffer never gives back what one big frame made it grow to.

The fix is to notice when the burst is over and start again from an idle-sized buffer. After 32 consecutive small batches, out_buf is replaced with a fresh BytesMut::with_capacity(4096) and the grown allocation is freed.

Why this cannot regress batching. The reset runs after the write completes, at a point where split() has already left out_buf empty and the waiters have been answered. No ceiling check, no carry-over decision, and no waiter observes it — the coalescing loop is not touched at all. The 29% fewer writes against 3.7% pong latency trade node_io.rs documents is unaffected because nothing about when or how frames are batched changed.

Why 32 and not 1. Shrinking after every large batch would make sustained large traffic realloc from 4 KiB up to 64 KiB per batch. The counter resets to zero on every large batch, so a burst spread over many batches is never interrupted mid-flight; only a session that has genuinely gone quiet pays the regrowth, once.

Changes

  • src/socket/noise_socket.rs: OUT_BUF_IDLE_CAPACITY / SMALL_BATCHES_BEFORE_SHRINK, two locals tracking whether the buffer is still holding a grown allocation and how long it has been quiet, and the reset after the write. One test.

Cost

before after
resident per socket after one 60 KiB frame + quiet traffic, median over 16, release 60.2 KiB 8.5 KiB
resident per socket, small traffic only 8.2 KiB 8.2 KiB

8.5 vs 8.2 KiB is the point: after the shrink a socket that once sent a large frame costs the same as one that never did.

What the report attributes: still nothing. This buffer remains unreadable from outside the task, so resource_report() does not name it before or after — the same reason #1235 documented it as unexpressible. The change reduces real memory the report never claimed, which means the gap between total_estimated_bytes() and RssAnon narrows by this much.

The side this makes worse: a session that goes quiet for 32 batches and then bursts again re-grows the deque, roughly four doublings of memcpy against a batch that is about to be AEAD-encrypted and pushed through TLS. Sustained large traffic never reaches the threshold, so it never pays this at all.

Checked and not changed

  • The batching ceiling. a_batch_never_overshoots_the_byte_ceiling still passes untouched, and the new test re-asserts the ceiling over the post-shrink burst as well.
  • The coalescing loop, carry_over, and the waiter bookkeeping. Not touched; the reset is strictly after the write.
  • MAX_BATCH_WIRE_BYTES and MAX_BATCH_FRAMES. Unchanged — this is about what the buffer keeps between batches, not what a batch may hold.

Validation

  • cargo fmt --all --check
  • cargo test -p whatsapp-rust --lib — 1442 passed
  • cargo clippy --workspace --all-targets -- -D warnings — clean (whatsapp-rust-voip-cli excluded locally: its build script needs alsa.pc, unavailable here; CI covers it)
  • Not run locally: the e2e suite — no reachable mock-server image in this environment. CI's E2E job drives real traffic through this sender.

New behaviour is covered by a_burst_after_the_buffer_shrinks_still_batches_and_round_trips, which grows the buffer with a 40 KiB frame, quietens past the threshold, then sends a fresh burst and asserts three things the shrink could plausibly have broken: the ceiling still holds, the regrown buffer still coalesces, and every frame decrypts in unbroken counter order across the whole connection.

One note on that test: the warm-up sends sequentially rather than through queue_all. Concurrent sends past the job channel's capacity of 8 do not queue in call order — the first draft did use join_all and failed with frames off by one, which is a property of the fixture, not of the sender.


Generated by Claude Code

One large stanza grows the sender's batch buffer to its own size, and split()
hands the written bytes over while the allocation stays behind, so the buffer
keeps that high-water mark for the rest of the connection. Measured per socket
in release, 16 sockets: 8.2 KiB after small traffic only, 60.2 KiB after a
single 60 KiB frame. A session that sends one media-sized stanza pays 52 KiB
for the rest of its life.

The buffer is now replaced with a fresh idle-sized one after 32 consecutive
small batches. That is off the batching path entirely: it runs after the write,
where split() has already left out_buf empty, so no ceiling check, carry-over
or waiter sees it. Sustained large traffic never reaches the threshold and
never shrinks; the cost lands only on a session that goes quiet and then bursts
again, which pays the deque growth once.

Marginal after a 60 KiB frame plus quiet traffic: 60.2 -> 8.5 KiB, level with a
socket that never sent a large frame at all.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jlucaso1, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 minutes

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

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c86cbb06-80dc-4632-b30e-9a811027ed4c

📥 Commits

Reviewing files that changed from the base of the PR and between 6b2730b and 59add6e.

📒 Files selected for processing (1)
  • src/socket/noise_socket.rs
📝 Walkthrough

Summary by CodeRabbit

  • Performance Improvements

    • Improved adaptive buffering for batched message transmission.
    • Buffers now automatically shrink after sustained smaller batches and grow again when needed.
  • Bug Fixes

    • Preserved batching behavior, frame ordering, size limits, encryption, and payload integrity when buffer capacity changes.

Walkthrough

The noise socket sender now adapts its batch-buffer capacity. It starts at 4 KiB, tracks oversized batches, reclaims the buffer after 32 small batches, and regrows it when required. An end-to-end test validates batching and payload integrity.

Changes

Adaptive batching buffer management

Layer / File(s) Summary
Buffer growth and reclamation policy
src/socket/noise_socket.rs
The sender records batch wire length, detects oversized batches, and replaces oversized allocations with a 4 KiB buffer after 32 subsequent small batches.
Buffer lifecycle regression coverage
src/socket/noise_socket.rs
The end-to-end test validates shrinking, regrowth, batching limits, counter order, encryption, and payload round trips.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: performance

Suggested reviewers: greptile-apps

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes releasing the socket batch buffer after a burst ends.
Description check ✅ Passed The description directly explains the buffer shrink behavior, design, impact, validation, and regression coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/per-session-marginal-noise-buffer

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.

@greptile-apps

greptile-apps Bot commented Aug 8, 2026

Copy link
Copy Markdown

Greptile Summary

The PR releases a sender-local batch allocation after sustained small traffic while preserving batching and wire behavior.

  • Adds an idle buffer capacity and a 32-small-batch release threshold.
  • Tracks grown-buffer state in the serialized Noise sender task.
  • Adds focused state-machine tests and an end-to-end post-release traffic test.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains.

No blocking failure remains.

Important Files Changed

Filename Overview
src/socket/noise_socket.rs Adds post-burst batch-buffer release state and accompanying sender-path tests.

Reviews (3): Last reviewed commit: "test(socket): tie the grown-capacity fix..." | Re-trigger Greptile

Comment thread src/socket/noise_socket.rs

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/socket/noise_socket.rs`:
- Around line 1206-1264: Extract the buffer reclamation policy around the
existing buffer transition into a testable helper, then add direct assertions
that a grown buffer is retained through small batch 31 and replaced by an
idle-capacity buffer after small batch 32. Keep the current end-to-end batching
and encryption test unchanged, including its wire-level assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ae7dbc09-18bc-46ba-873a-638a9b99bc47

📥 Commits

Reviewing files that changed from the base of the PR and between c71fb02 and 6b2730b.

📒 Files selected for processing (1)
  • src/socket/noise_socket.rs

Comment thread src/socket/noise_socket.rs
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 10.03 MiB 10.03 MiB +256 B (+0.00%) 🔺
bin .text 8.04 MiB 8.04 MiB +256 B (+0.00%) 🔺
bin allocated (text+data+bss) 10.03 MiB 10.03 MiB 0
llvm-lines wacore 520,664 520,664 0
llvm-lines wacore copies 17,009 17,009 0
llvm-lines whatsapp-rust lib 741,035 741,096 +61 (+0.01%) 🔺
llvm-lines whatsapp-rust lib copies 23,274 23,276 +2 (+0.01%) 🔺
deps crates (Cargo.lock) 462 462 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.83 MiB 1.83 MiB +239 B (+0.01%) 🔺
.text wacore 696.10 KiB 695.80 KiB -312 B (-0.04%) 🔽
.text wacore_binary 88.30 KiB 88.30 KiB 0
.text wacore_libsignal 178.88 KiB 178.88 KiB 0
.text wacore_appstate 22.35 KiB 22.35 KiB 0
.text wacore_noise 20.94 KiB 20.94 KiB 0
.text waproto 1.74 MiB 1.74 MiB 0
.text whatsapp_rust_sqlite_storage 540.30 KiB 540.30 KiB 0
.text whatsapp_rust_tokio_transport 40.49 KiB 40.49 KiB 0
.text whatsapp_rust_ureq_http_client 12.68 KiB 12.68 KiB 0
.text std 993.99 KiB 993.99 KiB 0
.text other deps 1.90 MiB 1.90 MiB +312 B (+0.02%) 🔺

Baseline: 272be18be (latest main run) · Head: 7a38084db · Graphs

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 1 file

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/socket/noise_socket.rs Outdated
Comment thread src/socket/noise_socket.rs
…wire

Two review findings, both valid.

A frame that fails mid-encrypt is truncated back out of the buffer, so the
allocation it grew survives with no wire bytes naming it and the countdown
never started. The decision now reads the buffer's capacity as well as the
batch length. Capacity only marks the buffer as grown, though: letting it also
restart the countdown would mean a grown buffer perpetually resetting the very
timer meant to release it.

The wire-level test could not observe any of this — out_buf and its two
counters live inside the sender task, so deleting the whole shrink left every
assertion in the file green. The decision is now a free function with its own
tests, verified by stubbing it out: two of the four fail when it does nothing.
greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 8, 2026

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 1 file (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/socket/noise_socket.rs Outdated
The truncated-frame case hard-coded 64 KiB, so a change to
OUT_BUF_IDLE_CAPACITY could take the fixture under the threshold and quietly
stop testing anything. Derived from the constant like its siblings.
@greptile-apps
greptile-apps Bot dismissed their stale review August 8, 2026 03:14

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@cubic-dev-ai cubic-dev-ai 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.

0 issues found across 1 file (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: The change introduces a memory-vs-CPU tradeoff: a 32-batch quiet threshold and 4 KiB idle capacity decide when to release a grown buffer, forcing reallocation on resumed bursts. That perf/capacity tuning choice needs human sign-off.

Re-trigger cubic

@jlucaso1

jlucaso1 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 59add6ecdc

ℹ️ 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".

@jlucaso1
jlucaso1 merged commit b904544 into main Aug 8, 2026
26 checks passed
@jlucaso1
jlucaso1 deleted the perf/per-session-marginal-noise-buffer branch August 8, 2026 05:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants