Repository navigation
Conversation
orgads
force-pushed
the
stream-slab-small-reads
branch
from
November 7, 2026 20:21
3b8f3af to
9fa96cc
Compare
lpinca
reviewed
Nov 7, 2026
Every read was carved out of the env-wide 64KB StreamReadSlab and handed to JS as a view over it. TLS asks for exactly the decrypted size, so the copy-on-waste path in Commit() never triggers, and any TLS chunk that JS keeps pins the whole slab and every other read in it. Chunks queued for seconds (e.g. audio streamed faster than real time) get the slabs promoted, and their external memory then drives most major GCs. Reads asking for less than a slab now get a right-sized managed buffer, as before the slab existed. Large reads (plain TCP, where libuv suggests 64KB) still use the slab. Assisted-by: Claude Code Signed-off-by: Orgad Shaneh <orgad.shaneh@audiocodes.com>
orgads
force-pushed
the
stream-slab-small-reads
branch
from
November 7, 2026 21:48
c1abe65 to
724c69a
Compare
lpinca
approved these changes
Nov 7, 2026
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #67327 +/- ##
=======================================
Coverage 90.68% 90.68%
=======================================
Files 797 797
Lines 279574 279577 +3
Branches 53778 53784 +6
=======================================
+ Hits 253525 253544 +19
+ Misses 16344 16306 -38
- Partials 9705 9727 +22
🚀 New features to boost your workflow:
|
Collaborator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
StreamReadSlabhands every stream read to JS as a view over an env-wide 64KB slab.Commit()copies a read out only when it wastes more than a quarter of its reservation, which covers plain TCP: libuv suggests 64KB, and small reads are copied and rewound. TLS asks for exactly the decrypted size (EmitAlloc(avail)inTLSWrap::ClearOut()), so the copy never triggers. Every TLS chunk is a view at slab offset 0, 320, 640, …, and one chunk that JS keeps pins the whole 64KB plus every other read in it.Large reads (plain TCP) still use the slab. TLS already
memcpys into the buffer it asked for, so it gets no extra copy, only one allocation per read, as before the slab was added.How this shows up in an application: we run a voice server with TTS audio over HTTPS. The audio arrives faster than real time and waits in JS queues for seconds. The chunks survive scavenges, so their slabs are promoted, and each pins 64KB of external memory. ArrayBuffer memory grew ~57MB between major GCs, and ~80% of major GCs started with
[IncrementalMarking] Start (external memory pressure)while the JS heap was far below its limit. On a busy 2-vCPU host each of those GCs stalled the event loop for 300–650ms.Evidence
New test
test/parallel/test-tls-small-read-own-buffer.js, which checks that each TLS chunk hasbyteOffset === 0andbuffer.byteLength === chunk.length.Before: fails. After: passes.
Retention, keeping 1 in 50 TLS chunks of a 3MB transfer, then
gc():Before (v26.12.1): kept 63KB of chunks,
arrayBuffers3.36MB (53x).After: 208KB (3.3x).
Production load (v26.12.1 with this patch, ~1000 concurrent calls on 2 processes):
Before: a major GC every 6.5s per process, and 11–23 audio underruns per 10-minute run.
After: a major GC every 25.5s, and 0 underruns.
Throughput,
benchmark/tls/throughput-s2c.js type=buf recvbuflen=0 dur=5, 10 alternating runs per binary on an idle 2-vCPU Debian 13 host. Both binaries are v26.12.1 from the same toolchain.Small writes pay one allocation per read. Large transfers gain, and their before-runs are much noisier (sd 1015 vs 215).
Tests:
tools/test.pyonparallel/test-{tls,net,stream,http,http2,https,child-process,pipe,buffer}-*gave 1664 passed and 6 failed. The same 6 fail on the unpatched build in my environment (getaddrinfo ENOTFOUND localhost, system CA).Merge Danger
Door: two-way. It changes only which allocator a read uses, so a revert restores the slab path.
Blast Radius: stream reads.
Commit(), but for TLS that is a secondmemcpy.🤖 Generated with Claude Code