Skip to content

[Fusion] Complete subgraph responses at the end of the JSON root - #10847

Merged
michaelstaib merged 2 commits into
mainfrom
perf/complete-at-json-root-end
Nov 10, 2026
Merged

michaelstaib merged 2 commits into
mainfrom
perf/complete-at-json-root-end

Conversation

@michaelstaib

Copy link
Copy Markdown
Member

Subgraphs send chunked responses, and the gateway waited for the final empty chunk before parsing, even when the JSON was already complete, which cost about 0.23 ms per request on the critical path. On HTTP/1.x the gateway now notices when the JSON root has ended and stops reading there, and the HTTP handler drains the rest when the response is disposed, so the connection stays reusable. HTTP/2 and HTTP/3 responses still read to the end of the body, because disposing an unfinished stream there resets it and the subgraph would see the request as aborted. Finding the end of the JSON uses a vectorized scan that takes about 12 us for the 87 KB benchmark response, and together with the drain it raises CPU per request by about 0.03 ms in exchange for the lower latency.

Metric Before Optimization After Optimization
Constant-latency req/s, AOT 3,353 3,402 (+1.5%)
Constant-latency p50 / p95 ms, AOT 13.87 / 19.31 13.68 / 18.80
Constant-latency req/s, JIT 3,407 3,475 (+2.0%)
Constant-latency p50 / p95 ms, JIT 13.64 / 19.16 13.39 / 18.51
Constant req/s, AOT 4,768 4,784 (+0.3%)
Constant req/s, JIT 5,041 5,152 (+2.2%)
CPU ms/req, constant-latency, AOT 0.787 0.826 (+5.0%)
CPU ms/req, constant, AOT 0.774 0.784 (+1.4%)
Allocated KiB/req, constant-latency, JIT 25.7 25.5
RSS avg / max MB, constant-latency, AOT 97.4 / 100.0 98.7 / 101.5
RSS avg / max MB, constant, AOT 105.2 / 118.4 94.3 / 110.1
Idle RSS MB, AOT 50.3 50.6
Startup ms, AOT 77 75

Measured on the benchmark twin with the official 16.7.0 subgraphs over HTTP/1.1 and the official gateway settings (alias batching, inline scheduling and inline socket completions), before is 592c379, after is this change on top of it without the HTTP/1.x restriction, which leaves the HTTP/1.1 path unchanged; means of 2-3 interleaved rounds with 40 s warm-up and 45 s measured, responses byte-identical.

With chunked transfer encoding, a source schema writes the terminating chunk when
its response completes, often in a later segment than the body, and the gateway read
every chunked body to its end. The arena read now finds the end of the root object or
array as bytes arrive and stops once the root ended and only whitespace followed it in
the same read. Disposing the content stream lets the handler drain the rest of the body,
which keeps the connection reusable. The root end is found with 64-byte vector masks.
On HTTP/2 and HTTP/3, disposing an unfinished response stream resets it, so the
source schema sees the request as aborted. Those responses now read to the end of
the body. The trailing-data test now awaits the read directly, and long documents
whose root does not end cover the vectorized scanner path.

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 was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@github-actions

Copy link
Copy Markdown
Contributor

Patch coverage

100.0% of changed lines covered (169/169)

File Covered Changed Patch %
…/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs 18 18 100.0% 🟢
…/src/Fusion.Execution/Transport/Http/JsonRootEndScanner.cs 151 151 100.0% 🟢

Project coverage: 59.5% (322222/541481 lines)

@michaelstaib
michaelstaib merged commit 7442cd2 into main Nov 10, 2026
159 checks passed
@michaelstaib
michaelstaib deleted the perf/complete-at-json-root-end branch November 10, 2026 12:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants