Skip to content

[Fusion] Write single JSON results straight into the response pipe - #10624

Merged
michaelstaib merged 7 commits into
mainfrom
tte/fusion-response-pipe-handoff
Oct 22, 2026
Merged

michaelstaib merged 7 commits into
mainfrom
tte/fusion-response-pipe-handoff

Conversation

@tobias-tengler

@tobias-tengler tobias-tengler commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

A single JSON result body is copied twice after it is formatted: once from the 64 KB staging buffer of ChunkedPipeBufferWriter into Kestrel's body writer, and again by Kestrel, because the response has not started yet and Kestrel has to move the buffered bytes behind the headers at the first flush.

This PR removes both copies with two separate commits. The first starts single results written as application/graphql-response+json or application/json right before the body is formatted (after all headers are set), so Kestrel writes the body straight into its output pipe. The second lets MinimizedJsonEmitter request 2 KB spans and lowers the ChunkedPipeBufferWriter direct-write threshold to 2 KB, so the minimized result is written into the response pipe memory instead of the staging buffer.

Stacked on #10617 (the base branch is tte/fusion-minimized-result-writer, so the diff shows only this change).

ResponseSerializationBenchmark (87 KB result), base is a tree with #10617 applied. FormatAsync_BlockPipe uses a pipe with 4 KB blocks like Kestrel's output pipe.

Method Version Mean Ratio Allocated Alloc Ratio
Format into buffer writer (no handoff) #10617 21.69 us 1.00 96 B 1.00
Format into buffer writer (no handoff) PR 21.79 us 1.00 96 B 1.00
Format + handoff, ArrayPool pipe #10617 25.45 us 1.00 168 B 1.00
Format + handoff, ArrayPool pipe PR 23.22 us 0.91 168 B 1.00
Format + handoff, 4 KB block pipe #10617 24.43 us 1.00 168 B 1.00
Format + handoff, 4 KB block pipe PR 22.82 us 0.93 168 B 1.00

k6 end-to-end screen (constant, A B B A, 3 x 30 s per invocation), base includes #10617:

Schema Run RPS base RPS PR RPS delta CPU/request base CPU/request PR CPU/request delta
Composite schema 2 A + 2 B 4891 4797 -1.9% 454 us 443 us -2.4%
Apollo Federation clean pair 4347 4595 +5.7% 417 us 397 us -4.9%
Apollo Federation stalled pair 2384 2417 +1.4% 383 us 369 us -3.7%

In the stalled Apollo Federation pair (one base and one PR invocation) the gateway sat at about 22% CPU and half the throughput, an environmental stall that hit both builds alike.

The handoff cost on top of plain formatting drops by about 60% in the micro. End to end, composite gateway CPU per request drops 2.4%, beyond main's A/A noise (CV 0.5 to 0.8%), and both PR invocations are below both base invocations; the composite RPS delta is within main's A/A noise (sigma about 65 RPS) and p99 is flat. Apollo Federation points the same way in both pairs, but with one clean pair it is supporting evidence only.

Behavior change: OnStarting callbacks now run before the body is serialized, and an exception thrown while serializing a single JSON result after the response started aborts the connection instead of producing a 500.

The composite result is written straight into the response buffer when the
writer neither indents nor omits nulls. Property names come from a token cached
on the selection that already holds the quotes and the colon, so no name is
checked for escaping, and every token is placed through one cached span.
The headers of a single result written as JSON are committed before the body is
formatted, so Kestrel writes the body straight into its output pipe instead of
buffering it until the first flush and copying it behind the headers.
The minimized result emitter requests spans of at least 2 KB, which the response
buffer writer now hands out from its underlying pipe, so the composite result is
no longer copied from the 64 KB staging buffer into the response pipe. Token by
token writers keep using the staging buffer.
@tobias-tengler
tobias-tengler changed the base branch from main to tte/fusion-minimized-result-writer October 19, 2026 06:44
Base automatically changed from tte/fusion-minimized-result-writer to main October 22, 2026 04:16
…ipe-handoff

# Conflicts:
#	src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Nodes/Selection.cs
#	src/HotChocolate/Fusion/src/Fusion.Execution/Text/Json/MinimizedJsonEmitter.cs
#	src/HotChocolate/Fusion/src/Fusion.Execution/Text/Utf8StringCache.cs
#	src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/ResponseFormattingTests.cs
@michaelstaib
michaelstaib marked this pull request as ready for review October 22, 2026 06:07
Copilot AI balanced review requested due to automatic review settings October 22, 2026 06:36

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 reached their quota limit.

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Patch coverage

100.0% of changed lines covered (6/6)

File Covered Changed Patch %
…/Formatters/DefaultHttpResponseFormatter.cs 4 4 100.0% 🟢
…/src/Transport.Formatters/EventStreamResultFormatter.cs 1 1 100.0% 🟢
…/AzureHttpResponse.cs 1 1 100.0% 🟢

Project coverage: 59.2% (314248/530638 lines)

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.

3 participants