Reduce async overhead in .NET GraphBinary reads#3517
Open
kirill-stepanishin wants to merge 1 commit into
Open
Conversation
Assisted-by: Claude Code:claude-opus-4-8
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
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.
Background
The GraphBinary response reader in Gremlin.Net used a
BufferedStreamso 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
BufferedStreamis replaced with a small customGraphBinaryReadBuffer. 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, sinceStream.ReadAsyncmay 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.
g.V().repeat(both()).times(2)(~200k results, latency)g.V()(6 results, latency)g.V()(throughput @ concurrency 64)g.V()(throughput @ concurrency 256)g.V()(throughput @ concurrency 1000)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.