Repository navigation
Conversation
… response headers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
The HTTP/2 and HTTP/3 custom-encoding fallback paths repeatedly format Content-Length on each Current read, increasing per-response string allocations rather than reusing a cached header pair.
2 open issues
What changed in this PR
This PR reduces Kestrel’s HTTP/2 and HTTP/3 response-header allocations by encoding Content-Length directly from its numeric value rather than creating an intermediate string.
Changes:
- Adds raw Content-Length enumeration with on-demand string formatting.
- Adds HPACK and QPACK numeric encoding paths while retaining the string path for custom encoding selectors.
- Adds byte-equivalence and small-buffer tests for both protocols.
| File | Description |
|---|---|
src/Servers/Kestrel/shared/KnownHeaders.cs |
Updates the generator to support raw Content-Length enumeration. |
src/Servers/Kestrel/shared/Http2HeadersEnumerator.cs |
Exposes numeric Content-Length to the HTTP/2 writer. |
src/Servers/Kestrel/shared/HPackHeaderWriter.cs |
Encodes Content-Length digits without an intermediate string. |
src/Servers/Kestrel/Core/test/Http3/Http3QPackEncoderTests.cs |
Tests QPACK byte equivalence and small-buffer behavior. |
src/Servers/Kestrel/Core/test/Http2/Http2HPackEncoderTests.cs |
Tests HPACK byte equivalence and small-buffer behavior. |
src/Servers/Kestrel/Core/src/Internal/Http3/QPackHeaderWriter.cs |
Adds numeric Content-Length encoding and preserves size accounting. |
src/Servers/Kestrel/Core/src/Internal/Http3/Http3HeadersEnumerator.cs |
Exposes numeric Content-Length to the HTTP/3 writer. |
src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseHeaders.cs |
Adds an internal raw Content-Length enumeration mode. |
src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs |
Updates generated enumeration to conditionally format Content-Length. |
Review effort: Balanced
With a custom ResponseHeaderEncodingSelector the HPACK/QPACK writers fall back to reading Current multiple times; format the value once and reuse it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/review |
Covers the HTTP/3 Content-Length encoding path, which the existing Kestrel microbenchmarks don't exercise directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Retriggering CI once (empty commit e87e555) after what looks like a Helix infrastructure failure.
🤖 Posted by ASP.NET Core Perf 2 (AI Agent) |
|
The Failing test: Why it's unrelated to this diff
Local runs at e87e555 (Linux x64)
E2E legs: The failures are Blazor-only tests. I won't retrigger again. A maintainer can rerun the failed jobs. 🤖 Posted by ASP.NET Core Perf 2 (AI Agent) |

Motivation
When Kestrel writes HTTP/2 or HTTP/3 response headers,
HttpResponseHeaders.EnumeratorformatsContentLength(along) into astring, and the HPACK/QPACK writers then encode that string. The runtime only caches number strings below 300, so every response withContent-Length >= 300allocates a string just to copy its digits into the output buffer.Change
HttpResponseHeaders.GetEnumeratorWithRawContentLength(): an enumerator mode, used only for response headers (not trailers), that does not format Content-Length. The generator (KnownHeaders.cs) andHttpHeaders.Generated.csare updated.Http2HeadersEnumerator/Http3HeadersEnumerator:TryGetCurrentContentLength(out long).Currentstill returns the formatted string, created lazily, so other consumers are unaffected.HPackHeaderWriter: when the current header is Content-Length and the default encoding selector is in use, it writes a literal header field without indexing (static name index 28) followed by the ASCII digits, usingUtf8Formatter. This matches the bytes produced today, because Content-Length is never added to the dynamic table.QPackHeaderWriter:content-length: 0).totalHeaderSizeaccounting is unchanged.ResponseHeaderEncodingSelectorfalls back to the existing string path.Benchmarks
Http2FrameWriterBenchmark.WriteResponseHeadersfromsrc/Servers/Kestrel/perf/Microbenchmarks, with MemoryDiagnoser.Environment:
--config perflab(InProcess),--warmupCount 5 --iterationCount 15.Tests
Http2HPackEncoderTests.BeginEncodeHeaders_ContentLength_MatchesStringEncodingandHttp3QPackEncoderTests.BeginEncodeHeaders_ContentLength_MatchesStringEncoding, for values 0, 9, 299, 300, 1024 andlong.MaxValue.*_BufferTooSmall_MatchesStringEncoding: for buffer sizes 1–31, the fast and string paths give the same result, the same bytes and the same remaining headers.Microsoft.AspNetCore.Server.Kestrel.Core.Tests: 9925 passed, 13 failed, 1 skipped. The 13 failures areAddressBinderTests/KestrelServerTestsIPv6 tests that fail identically onmain, because the test machine has no IPv6.InMemory.FunctionalTests(filter Http2|Http3): 775 passed, 0 failed, 5 skipped.Benchmark (3 interleaved rounds)
Http2FrameWriterBenchmark.WriteResponseHeaders: existing; H2 path with Content-Length 1024.QPackHeaderWriterBenchmark.BeginEncodeHeaders: new in this PR, commit 5442cf0, undersrc/Servers/Kestrel/perf/Microbenchmarks/Http3. It covers the H3/QPACK path with Content-Length 0 and 1024, which no existing benchmark exercised directly.HPackHeaderWriterBenchmark: existing control; it sets Content-Length 0.The same benchmark source was compiled into the main build.
Environment: AMD EPYC 9V74, 2 vCPU (container), Ubuntu 24.04; .NET SDK 12.0.100-alpha.1.26521.103, .NET 12.0.0 (12.0.26.52911); BenchmarkDotNet v0.13.0
--config perflab(InProcess, MemoryDiagnoser), WarmupCount=3, IterationCount=10. "main" is the merge base4f556f73a6. Both builds use the same benchmark source, and runs were interleaved main, PR, main, PR, main, PR. Each cell shows mean / allocated per operation; allocation figures are exact bytes from the CSV exporter.LargeHeaderSize=0is 11% faster and QPACKContentLength=1024is 16% faster, with no overlap between main and PR. TheLargeHeaderSize=10/20cases are dominated by the large custom header, and their time difference (about −4%) is within noise.ContentLength=0(static table entry) is unchanged.HPackHeaderWriterBenchmark(Content-Length 0, which main serves from a cached"0"string): theUnknownHeadersvariants are about 3% slower in the PR in all 3 rounds (411–417 ns vs 424–431 ns). TheKnownHeadersvariants range from −0.5% to +4% per round. Zero bytes are allocated on both sides. A plausible cause is the extra per-header Content-Length check and formatting the digits instead of reusing the cached string, but I haven't verified that.🤖 Posted by ASP.NET Core Perf 2 (AI Agent)