Skip to content

Commit aaa8589

Browse files
committed
internal/transport: optimize header field allocation
1 parent 03255a9 commit aaa8589

2 files changed

Lines changed: 96 additions & 7 deletions

File tree

internal/transport/http2_server.go

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,19 @@ func appendHeaderFieldsFromMD(headerFields []hpack.HeaderField, md metadata.MD)
945945
return headerFields
946946
}
947947

948+
// headerFieldsCountFromMD returns the total number of header fields that
949+
// appendHeaderFieldsFromMD will produce from md. Reserved headers are dropped
950+
// by appendHeaderFieldsFromMD, so this may slightly over-count, which is
951+
// preferable to growing the slice.
952+
func headerFieldsCountFromMD(md metadata.MD) int {
953+
n := 0
954+
for _, vv := range md {
955+
n += len(vv)
956+
}
957+
958+
return n
959+
}
960+
948961
func (t *http2Server) checkForHeaderListSize(hf []hpack.HeaderField) bool {
949962
if t.maxSendHeaderListSize == nil {
950963
return true
@@ -1041,9 +1054,14 @@ func (t *http2Server) writeHeader(s *ServerStream, md metadata.MD) error {
10411054
}
10421055

10431056
func (t *http2Server) writeHeaderLocked(s *ServerStream) error {
1044-
// TODO(mmukhi): Benchmark if the performance gets better if count the metadata and other header fields
1045-
// first and create a slice of that exact size.
1046-
headerFields := make([]hpack.HeaderField, 0, 2) // at least :status, content-type will be there if none else.
1057+
// Count the metadata header fields as well so the slice is not reallocated
1058+
// while they are appended below.
1059+
headerFieldCount := 2 // :status, content-type
1060+
headerFieldCount += headerFieldsCountFromMD(s.header)
1061+
if s.sendCompress != "" {
1062+
headerFieldCount++
1063+
}
1064+
headerFields := make([]hpack.HeaderField, 0, headerFieldCount)
10471065
headerFields = append(headerFields, hpack.HeaderField{Name: ":status", Value: "200"})
10481066
headerFields = append(headerFields, hpack.HeaderField{Name: "content-type", Value: grpcutil.ContentType(s.contentSubtype)})
10491067
if s.sendCompress != "" {
@@ -1087,10 +1105,18 @@ func (t *http2Server) writeStatus(s *ServerStream, st *status.Status) error {
10871105
return nil
10881106
}
10891107

1090-
// TODO(mmukhi): Benchmark if the performance gets better if count the metadata and other header fields
1091-
// first and create a slice of that exact size.
1092-
headerFields := make([]hpack.HeaderField, 0, 2) // grpc-status and grpc-message will be there if none else.
1093-
if !s.updateHeaderSent() { // No headers have been sent.
1108+
// Count the metadata header fields as well so the slice is not reallocated
1109+
// while they are appended below.
1110+
headersAlreadySent := s.updateHeaderSent()
1111+
headerFieldCount := 2 // grpc-status, grpc-message
1112+
headerFieldCount += headerFieldsCountFromMD(s.trailer)
1113+
if !headersAlreadySent && len(s.header) == 0 {
1114+
// Trailer only response gets :status and content-type as well.
1115+
headerFieldCount += 2
1116+
}
1117+
// +1 for optional grpc-status-details-bin
1118+
headerFields := make([]hpack.HeaderField, 0, headerFieldCount+1)
1119+
if !headersAlreadySent { // No headers have been sent.
10941120
if len(s.header) > 0 { // Send a separate header frame.
10951121
if err := t.writeHeaderLocked(s); err != nil {
10961122
return err
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
*
3+
* Copyright 2026 gRPC authors.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package transport
20+
21+
import (
22+
"fmt"
23+
"testing"
24+
25+
"golang.org/x/net/http2/hpack"
26+
27+
"google.golang.org/grpc/metadata"
28+
)
29+
30+
// BenchmarkAppendHeaderFieldsFromMD measures the slice re-allocation overhead
31+
// of appendHeaderFieldsFromMD when the initial slice capacity is too small
32+
// (old behavior) versus pre-sized to the exact metadata count (new behavior).
33+
func BenchmarkAppendHeaderFieldsFromMD(b *testing.B) {
34+
for _, mdCount := range []int{0, 4, 12, 48} {
35+
b.Run(fmt.Sprintf("mdCount=%d", mdCount), func(b *testing.B) {
36+
md := make(metadata.MD, mdCount)
37+
for i := 0; i < mdCount; i++ {
38+
md[fmt.Sprintf("header-%d", i)] = []string{
39+
fmt.Sprintf("value-%d-a", i),
40+
fmt.Sprintf("value-%d-b", i),
41+
}
42+
}
43+
44+
b.Run("unsized", func(b *testing.B) {
45+
b.ReportAllocs()
46+
for b.Loop() {
47+
headerFields := make([]hpack.HeaderField, 0, 2)
48+
headerFields = appendHeaderFieldsFromMD(headerFields, md)
49+
_ = headerFields
50+
}
51+
})
52+
53+
b.Run("sized", func(b *testing.B) {
54+
b.ReportAllocs()
55+
for b.Loop() {
56+
headerFields := make([]hpack.HeaderField, 0, headerFieldsCountFromMD(md))
57+
headerFields = appendHeaderFieldsFromMD(headerFields, md)
58+
_ = headerFields
59+
}
60+
})
61+
})
62+
}
63+
}

0 commit comments

Comments
 (0)