Skip to content

Commit 3031bf4

Browse files
net/http/internal/http2: enable SetReuseFrames in server and Transport
Call Framer.SetReuseFrames once on the per-connection Framer in both serverConn (serveConn) and Transport (newClientConn) so the parsed *DataFrame, *WindowUpdateFrame, *HeadersFrame, and *MetaHeadersFrame structs returned by ReadFrame are reused across calls instead of being heap-allocated each time. SetReuseFrames has shipped as an opt-in Framer method since CL 34812 (golang/net, 2017) but no in-tree caller previously opted in, so the cache was effectively dead code from the standard library's perspective; this CL turns it on. A new GODEBUG setting, http2reuseframes, is registered as a compat escape hatch in line with the existing http2client / http2server / http2debug settings: - default (or http2reuseframes=1): reuse on, this CL's behavior. - GODEBUG=http2reuseframes=0: reuse disabled, pre-CL behavior. If a deployment encounters an issue the static audit and -race coverage didn't catch, operators can flip the setting without recompiling. Per-frame allocation savings, microbench numbers (linux/amd64, from the BenchmarkParse* and BenchmarkReadMetaFrame benchmarks added earlier in this stack): bench Default Reused ParseDataFrame 48 B, 1 alloc 0 B, 0 alloc ParseWindowUpdateFrame 16 B, 1 alloc 0 B, 0 alloc ParseHeadersFrame 48 B, 1 alloc 0 B, 0 alloc ReadMetaFrame 472 B, 9 allocs 376 B, 7 allocs End-to-end allocation reductions on the package's existing read-path benchmarks (-count=10, benchstat master vs branch): bench allocs/op delta p ClientGzip -59.20% 0.000 DownloadFrameSize/16k -46.73% 0.000 DownloadFrameSize/64k -49.00% 0.000 DownloadFrameSize/128k -49.55% 0.000 DownloadFrameSize/256k -49.87% 0.000 DownloadFrameSize/512k -49.98% 0.000 ClientRequestHeaders/0 -8.00% 0.000 ClientResponseHeaders/0 -8.00% 0.000 ClientRequestHeaders/10 -5.41% 0.000 ClientResponseHeaders/10 -3.48% 0.000 geomean (allocs/op) -19.46% Write-side benchmarks (WriteScheduler*, WriteQueue) are unchanged, as expected; those paths do not exercise ReadFrame. ClientGzip latency also improves by -2.74% (p=0.023). The remaining allocations in ReadMetaFrame come from HPACK Fields-slice growth and the SetEmitFunc closure, which this CL does not address. Reuse safety, by frame type: DataFrame serverConn.processData and clientConnReadLoop.processData read Length, StreamID, StreamEnded, and the Data() slice synchronously before returning. The bytes from Data() flow through {server,client}Conn.body / cs.bufPipe -> dataBuffer.Write, which copies into a pool-allocated chunk; no slice retained past ReadFrame. WindowUpdateFrame serverConn.processWindowUpdate and clientConnReadLoop.processWindowUpdate read only the scalar StreamID and Increment fields. The struct type has no slice fields, so there is nothing to alias the read buffer. HeadersFrame Both server and Transport set Framer.ReadMetaHeaders during init, so a bare *HeadersFrame is never delivered to consumer code; the Framer always returns *MetaHeadersFrame on a HEADERS frame. readMetaFrame clears MetaHeadersFrame.HeadersFrame.headerFragBuf and calls invalidate() on the embedded *HeadersFrame before returning. The aliased frag buf is therefore not exposed past readMetaFrame. MetaHeadersFrame The Fields slice is freshly allocated per parse: readMetaFrame does *mh = MetaHeadersFrame{HeadersFrame: hf} (Fields zeroed to nil), then the HPACK emit callback grows it via append, so each returned MetaHeadersFrame has its own backing array. HPACK Name/Value strings are independently allocated by the decoder (hpack.decodeString returns string(u.b) / buf.String(), both of which copy), so no string aliases the read buffer. serverConn.processHeaders / processTrailerHeaders and clientConnReadLoop.processHeaders / handleResponse / processTrailers iterate Fields synchronously on the read-loop / serve goroutine and copy strings into a fresh http.Header before returning. No code stores *MetaHeadersFrame past the dispatch. Server-side gating: readFrames -> readFrameCh -> serve calls processFrameFromReader synchronously and only then invokes readMore(), which unblocks readFrames for the next ReadFrame. So even though the server consumes frames on a different goroutine from the one calling ReadFrame, every frame is fully consumed before the cache is overwritten. Transport-side gating: clientConnReadLoop.run consumes each frame synchronously on the read-loop goroutine before the next ReadFrame, and what escapes to the RoundTrip / response-body goroutines is either copied (DataFrame -> bufPipe) or composed of immutable, independently-allocated Go strings (Header maps). While here, defensively clear GoAwayFrame.debugData after copying it to cc.goAwayDebug in processGoAway. GoAwayFrame is not in frameCache today so the retained cc.goAway pointer is safe, but this Transport already stores a *GoAwayFrame across ReadFrame calls; clearing the only field that aliases the read buffer prevents a future addition of GoAwayFrame to the reuse cache from silently turning cc.goAway.DebugData() into a use-after-overwrite. Verification: net/http/internal/http2: go test -race -count=30 PASS (8m13s) net/http/internal/http2: go test -race -count=10 -cpu=1,2,4,8 PASS (6m17s) net/http: go test -race -count=5 PASS (2m15s) TestFrameReuseRaceCorrect -race -count=200 PASS (5s) TestFrameReuseRaceAdversarial under H2_REUSE_RACE_NEGATIVE=1 -race -count=10 RACE (10/10) TestFrameReuseEndToEndStress -race PASS GODEBUG=http2reuseframes=0 -race (TestFrameReuseEndToEndStress, TestFrameReuseRaceCorrect) PASS The adversarial test deliberately violates the reuse contract and asserts the race detector fires; the other runs validate the production code paths both with reuse on (the default) and with reuse disabled via GODEBUG. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Change-Id: I9d6d0c2e761b0901314c25f362c99062260b31a7
1 parent e1978bc commit 3031bf4

6 files changed

Lines changed: 38 additions & 0 deletions

File tree

doc/godebug.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,14 @@ Setting `x509sslcertoverrideplatform=0` disables this behavior in favor of using
188188
the platform certificate store instead of honoring the environment variables. We
189189
plan to remove this setting in Go 1.31.
190190

191+
Go 1.27 added a new `http2reuseframes` setting that controls whether the
192+
net/http HTTP/2 server and Transport opt in to `Framer.SetReuseFrames`,
193+
which reuses parsed `*DataFrame`, `*WindowUpdateFrame`, `*HeadersFrame`,
194+
and `*MetaHeadersFrame` structs across `ReadFrame` calls to reduce
195+
per-frame heap allocation. The default is reuse on. Setting
196+
`http2reuseframes=0` reverts to allocating each parsed frame fresh,
197+
matching the pre-Go 1.27 behavior.
198+
191199
### Go 1.26
192200

193201
Go 1.26 added a new `httpcookiemaxnum` setting that controls the maximum number

src/internal/godebugs/table.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ var All = []Info{
4242
{Name: "htmlmetacontenturlescape", Package: "html/template"},
4343
{Name: "http2client", Package: "net/http"},
4444
{Name: "http2debug", Package: "net/http", Opaque: true},
45+
{Name: "http2reuseframes", Package: "net/http"},
4546
{Name: "http2server", Package: "net/http"},
4647
{Name: "httpcookiemaxnum", Package: "net/http", Changed: 24, Old: "0"},
4748
{Name: "httplaxcontentlength", Package: "net/http", Changed: 22, Old: "1"},

src/net/http/internal/http2/frame.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"encoding/binary"
1010
"errors"
1111
"fmt"
12+
"internal/godebug"
1213
"io"
1314
"log"
1415
"slices"
@@ -22,6 +23,12 @@ import (
2223
"golang.org/x/net/http/httpguts"
2324
)
2425

26+
// http2reuseframes controls whether the per-connection Framer in the
27+
// stdlib server and Transport opts in to SetReuseFrames. Default is
28+
// reuse on; GODEBUG=http2reuseframes=0 reverts to allocating each
29+
// parsed frame fresh, matching the pre-CL behavior.
30+
var http2reuseframes = godebug.New("http2reuseframes")
31+
2532
const frameHeaderLen = 9
2633

2734
var padZeros = make([]byte, 255) // zeros for padding

src/net/http/internal/http2/server.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,11 @@ func (s *Server) serveConn(c net.Conn, opts *ServeConnOpts, newf func(*serverCon
322322
fr.ReadMetaHeaders = hpack.NewDecoder(uint32(conf.MaxDecoderHeaderTableSize), nil)
323323
fr.MaxHeaderListSize = sc.maxHeaderListSize()
324324
fr.SetMaxReadFrameSize(uint32(conf.MaxReadFrameSize))
325+
if http2reuseframes.Value() == "0" {
326+
http2reuseframes.IncNonDefault()
327+
} else {
328+
fr.SetReuseFrames()
329+
}
325330
sc.framer = fr
326331

327332
if tc, ok := c.(connectionStater); ok {

src/net/http/internal/http2/transport.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,11 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool, internalStateHook
662662
maxHeaderTableSize := uint32(conf.MaxDecoderHeaderTableSize)
663663
cc.fr.ReadMetaHeaders = hpack.NewDecoder(maxHeaderTableSize, nil)
664664
cc.fr.MaxHeaderListSize = t.maxHeaderListSize()
665+
if http2reuseframes.Value() == "0" {
666+
http2reuseframes.IncNonDefault()
667+
} else {
668+
cc.fr.SetReuseFrames()
669+
}
665670

666671
cc.henc = hpack.NewEncoder(&cc.hbuf)
667672
cc.henc.SetMaxDynamicTableSizeLimit(uint32(conf.MaxEncoderHeaderTableSize))
@@ -2626,6 +2631,13 @@ func (rl *clientConnReadLoop) processGoAway(f *GoAwayFrame) error {
26262631
if cc.goAwayDebug == "" {
26272632
cc.goAwayDebug = string(f.DebugData())
26282633
}
2634+
// debugData aliases the Framer's read buffer. SetReuseFrames does
2635+
// not cache GoAwayFrame today, so cc.goAway = f is safe, but we
2636+
// retain the *GoAwayFrame across ReadFrame calls; clear the only
2637+
// field that aliases the read buffer so a future addition of
2638+
// GoAwayFrame to frameCache cannot turn cc.goAway.DebugData() into
2639+
// a use-after-overwrite.
2640+
f.debugData = nil
26292641
if old != nil && old.ErrCode != ErrCodeNo {
26302642
cc.goAway.ErrCode = old.ErrCode
26312643
}

src/runtime/metrics/doc.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,11 @@ Below is the full list of supported metrics, ordered lexicographically.
311311
The number of non-default behaviors executed by the net/http
312312
package due to a non-default GODEBUG=http2client=... setting.
313313
314+
/godebug/non-default-behavior/http2reuseframes:events
315+
The number of non-default behaviors executed by the net/http
316+
package due to a non-default GODEBUG=http2reuseframes=...
317+
setting.
318+
314319
/godebug/non-default-behavior/http2server:events
315320
The number of non-default behaviors executed by the net/http
316321
package due to a non-default GODEBUG=http2server=... setting.

0 commit comments

Comments
 (0)