Repository navigation
Increase stream-backed async deserialization buffer to 64 KB - #1079
Merged
Merged
Conversation
Increase the StreamPipeReader buffer to 64 KB so large payloads feed the streaming converters in useful batches while remaining below the LOH threshold. Add a benchmark covering payloads below and above the 1 MB synchronous fast-path threshold. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
No unresolved blocking issues were identified.
Review effort: Lite
Findings: None
What changed in this PR
Increases the default stream-backed async deserialization buffer to 64 KB and adds performance benchmarks.
Changes:
- Configures a shared 64 KB stream reader buffer.
- Adds benchmarks comparing buffer sizes and synchronous deserialization.
| File | Description |
|---|---|
test/Benchmarks/AsyncStreamDeserialization.cs |
Adds async stream deserialization benchmarks. |
src/Nerdbank.MessagePack/MessagePackSerializer.FriendlyOverloads.cs |
Sets the default stream reader buffer to 64 KB. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## v1.3 #1079 +/- ##
==========================================
+ Coverage 76.51% 76.58% +0.07%
==========================================
Files 175 175
Lines 13477 13477
Branches 2727 2727
==========================================
+ Hits 10312 10322 +10
+ Misses 2292 2276 -16
- Partials 873 879 +6
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Merged
This was referenced Oct 9, 2026
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.
What
Increases the default
StreamPipeReaderOptionsbuffer size used by theStream-based async deserialization overloads (DeserializeAsync,DeserializeObjectAsync,DeserializeEnumerableAsync,DeserializePathEnumerableAsync) from the .NET default (4 KB) to 64 KB.Why
Issue reports (see #1078) showed that async deserialization from a
Streamwas dramatically slower than expected for large payloads (a 50 MB response took ~2m28s vs. ~16s when pre-buffered).MaxAsyncBuffer(default 1 MB) only produces a fast, fully-synchronous read path when the entire payload arrives before that threshold is reached; once a stream is large enough that EOF isn't seen within 1 MB, deserialization falls back to the streaming/async converter path. That path repeatedly asks thePipeReaderfor whatever it currently has buffered (GetBufferedStructuresCount/TrySkip), so with the framework's default 4 KB buffer it was scanning tiny batches over and over, which dominated CPU time.64 KB stays comfortably below the 85 KB large-object-heap threshold (see the existing
ReadAtLeastNoLOHAsyncworkaround) while giving the streaming converters meaningfully larger batches to work with per read.Users can wrap the
Streamthemselves viaPipeReader.Createand use theDeserializeAsync(PipeReader, ...)overloads in order to customize the buffer size (e.g. grow it further for improved performance at the possible cost of GC pressure).Evidence
Added a new benchmark (
test/Benchmarks/AsyncStreamDeserialization.cs) that deserializes an array of 10,000/50,000 POCOs through a non-seekable stream with varyingStreamPipeReaderOptionsbuffer sizes (4 KB, 64 KB, 256 KB, 1 MB), alongside the default-buffer stream overload and a fully-synchronous baseline for comparison.For 50,000 elements (BenchmarkDotNet, medium job):
64 KB was the best-performing buffer size tested locally in this synthetic benchmark; larger buffers regressed slightly, likely due to increased per-read copy/allocation cost outweighing the reduction in read-call overhead for this workload shape. The original issue reporter measured a much larger real-world improvement (their end-to-end scenario went from ~2m28s to ~16-24s with larger buffers over a real network connection), where read-syscall/network latency overhead is much more significant than in this in-memory benchmark.
Notes for reviewers
PipeReaderfrom a caller-suppliedStream. It does not affect callers who construct and pass their ownPipeReader.MaxAsyncBuffer(the pre-buffering threshold that enables the synchronous fast path) is unchanged.StreamReadBufferSizeonMessagePackSerializer) so callers with very large payloads and abundant memory can opt into larger buffers; that is not included in this PR.