Skip to content

Reduce async overhead in .NET GraphBinary reads#3517

Open
kirill-stepanishin wants to merge 1 commit into
apache:masterfrom
kirill-stepanishin:dotnet-graphbinary-custom-read-buffer
Open

Reduce async overhead in .NET GraphBinary reads#3517
kirill-stepanishin wants to merge 1 commit into
apache:masterfrom
kirill-stepanishin:dotnet-graphbinary-custom-read-buffer

Conversation

@kirill-stepanishin

Copy link
Copy Markdown
Contributor

Background

The GraphBinary response reader in Gremlin.Net used a BufferedStream so it could refill from the network in 8 KiB chunks. But even when the next few bytes were already sitting in that buffer, every primitive read still went through an async state machine and allocated a small scratch array. On large responses, that per-read overhead adds up to a large share of deserialization time.

The change

BufferedStream is replaced with a small custom GraphBinaryReadBuffer. Refill behavior is the same, but when the bytes are already buffered the typed readers complete synchronously with no allocation. When they are not, a slow path handles the refill and keeps the previous EOF and cancellation behavior.

Variable-length payloads (Binary, String, Char) also switch to ReadExactlyAsync, since Stream.ReadAsync may return fewer bytes than requested and the old code assumed it wouldn't.

Unit tests cover the common case where bytes are already buffered, cases where a value straddles a refill boundary, end of stream, cancellation, and reading a full response one small chunk at a time through ResponseSerializer.

Performance

Benchmarked with the client and server on separate cross-region EC2 instances (server in US-EAST-2, client in US-WEST-2) so results reflect realistic network latency.

Workload Before After Change
g.V().repeat(both()).times(2) (~200k results, latency) 2.229 s 1.612 s 28% faster
g.V() (6 results, latency) 0.0578 s 0.0580 s no change
g.V() (throughput @ concurrency 64) 1,145 req/s 1,151 req/s no change
g.V() (throughput @ concurrency 256) 4,597 req/s 4,595 req/s no change
g.V() (throughput @ concurrency 1000) 17,402 req/s 17,423 req/s no change

Large responses benefit the most, since that is where async overhead accounts for most of the client-side time. Throughput is unchanged, and the small query confirms no regression.

Assisted-by: Claude Code:claude-opus-4-8
@codecov-commenter

codecov-commenter commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.41%. Comparing base (a28cd1f) to head (d089e15).
⚠️ Report is 270 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3517      +/-   ##
============================================
+ Coverage     76.35%   76.41%   +0.05%     
- Complexity    13424    14268     +844     
============================================
  Files          1012     1036      +24     
  Lines         60341    64516    +4175     
  Branches       7075     7648     +573     
============================================
+ Hits          46076    49300    +3224     
- Misses        11548    12123     +575     
- Partials       2717     3093     +376     

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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