Skip to content

Increase stream-backed async deserialization buffer to 64 KB - #1079

Merged
AArnott merged 1 commit into
v1.3from
aarnott-miniature-funicular
Sep 22, 2026
Merged

AArnott merged 1 commit into
v1.3from
aarnott-miniature-funicular

Conversation

@AArnott

@AArnott AArnott commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner

What

Increases the default StreamPipeReaderOptions buffer size used by the Stream-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 Stream was 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 the PipeReader for 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 ReadAtLeastNoLOHAsync workaround) while giving the streaming converters meaningfully larger batches to work with per read.

Users can wrap the Stream themselves via PipeReader.Create and use the DeserializeAsync(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 varying StreamPipeReaderOptions buffer 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):

Method Mean Allocated
Buffer4KB (baseline) 23.47 ms 8.46 MB
Buffer64KB 22.59 ms 8.40 MB
Buffer256KB 27.73 ms 8.39 MB
Buffer1MB 28.14 ms 8.39 MB
Synchronous 12.98 ms 8.39 MB

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

  • This only changes the buffer size used internally when the library creates a PipeReader from a caller-supplied Stream. It does not affect callers who construct and pass their own PipeReader.
  • MaxAsyncBuffer (the pre-buffering threshold that enables the synchronous fast path) is unchanged.
  • A follow-up could expose this buffer size as a serializer-level setting (e.g. StreamReadBufferSize on MessagePackSerializer) so callers with very large payloads and abundant memory can opt into larger buffers; that is not included in this PR.

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>
Copilot AI lite review requested due to automatic review settings September 22, 2026 17:21
@AArnott AArnott added this to the 1.3 milestone Sep 22, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

codecov Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.58%. Comparing base (c40c4fc) to head (768917d).
⚠️ Report is 2 commits behind head on v1.3.

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     
Flag Coverage Δ
Linux 75.64% <100.00%> (+0.07%) ⬆️
Windows 76.22% <100.00%> (ø)
macOS 75.67% <100.00%> (+0.11%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

2 participants