Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 33 additions & 7 deletions internal/transport/http2_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,19 @@ func appendHeaderFieldsFromMD(headerFields []hpack.HeaderField, md metadata.MD)
return headerFields
}

// headerFieldsCountFromMD returns the total number of header fields that
// appendHeaderFieldsFromMD will produce from md. Reserved headers are dropped
// by appendHeaderFieldsFromMD, so this may slightly over-count, which is
// preferable to growing the slice.
Comment thread
2307vivek marked this conversation as resolved.
func headerFieldsCountFromMD(md metadata.MD) int {
n := 0
for _, vv := range md {
n += len(vv)
}

return n
}
Comment thread
2307vivek marked this conversation as resolved.

func (t *http2Server) checkForHeaderListSize(hf []hpack.HeaderField) bool {
if t.maxSendHeaderListSize == nil {
return true
Expand Down Expand Up @@ -1041,9 +1054,14 @@ func (t *http2Server) writeHeader(s *ServerStream, md metadata.MD) error {
}

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

// TODO(mmukhi): Benchmark if the performance gets better if count the metadata and other header fields
// first and create a slice of that exact size.
headerFields := make([]hpack.HeaderField, 0, 2) // grpc-status and grpc-message will be there if none else.
if !s.updateHeaderSent() { // No headers have been sent.
// Count the metadata header fields as well so the slice is not reallocated
// while they are appended below.
headersAlreadySent := s.updateHeaderSent()
headerFieldCount := 2 // grpc-status, grpc-message
headerFieldCount += headerFieldsCountFromMD(s.trailer)
if !headersAlreadySent && len(s.header) == 0 {
// Trailer only response gets :status and content-type as well.
headerFieldCount += 2
}
// +1 for optional grpc-status-details-bin
headerFields := make([]hpack.HeaderField, 0, headerFieldCount+1)
Comment thread
2307vivek marked this conversation as resolved.
Outdated
if !headersAlreadySent { // No headers have been sent.
if len(s.header) > 0 { // Send a separate header frame.
if err := t.writeHeaderLocked(s); err != nil {
return err
Expand Down
63 changes: 63 additions & 0 deletions internal/transport/http2_server_header_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
*
* Copyright 2026 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package transport

import (
"fmt"
"testing"

"golang.org/x/net/http2/hpack"

"google.golang.org/grpc/metadata"
)

// BenchmarkAppendHeaderFieldsFromMD measures the slice re-allocation overhead
// of appendHeaderFieldsFromMD when the initial slice capacity is too small
// (old behavior) versus pre-sized to the exact metadata count (new behavior).
func BenchmarkAppendHeaderFieldsFromMD(b *testing.B) {
for _, mdCount := range []int{0, 4, 12, 48} {
b.Run(fmt.Sprintf("mdCount=%d", mdCount), func(b *testing.B) {
md := make(metadata.MD, mdCount)
for i := 0; i < mdCount; i++ {
md[fmt.Sprintf("header-%d", i)] = []string{
fmt.Sprintf("value-%d-a", i),
fmt.Sprintf("value-%d-b", i),
}
}

b.Run("unsized", func(b *testing.B) {
Comment thread
2307vivek marked this conversation as resolved.
Outdated
b.ReportAllocs()
for b.Loop() {
headerFields := make([]hpack.HeaderField, 0, 2)
headerFields = appendHeaderFieldsFromMD(headerFields, md)
_ = headerFields
}
})

b.Run("sized", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
headerFields := make([]hpack.HeaderField, 0, headerFieldsCountFromMD(md))
headerFields = appendHeaderFieldsFromMD(headerFields, md)
_ = headerFields
}
})
})
}
}
Loading