Skip to content

Commit 44e3be2

Browse files
committed
TUN-3209: improve performance and reduce allocations during user header serialization from h1 to h2
benchmark old ns/op new ns/op delta BenchmarkH1ResponseToH2ResponseHeaders-4 10360 5048 -51.27% benchmark old allocs new allocs delta BenchmarkH1ResponseToH2ResponseHeaders-4 135 26 -80.74% benchmark old bytes new bytes delta BenchmarkH1ResponseToH2ResponseHeaders-4 8543 3667 -57.08%
1 parent 61d5461 commit 44e3be2

File tree

2 files changed

+73
-72
lines changed

2 files changed

+73
-72
lines changed

h2mux/header.go

Lines changed: 55 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,12 @@ const (
3838
// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3
3939
func H2RequestHeadersToH1Request(h2 []Header, h1 *http.Request) error {
4040
for _, header := range h2 {
41-
if !IsControlHeader(header.Name) {
41+
name := strings.ToLower(header.Name)
42+
if !IsControlHeader(name) {
4243
continue
4344
}
4445

45-
switch strings.ToLower(header.Name) {
46+
switch name {
4647
case ":method":
4748
h1.Method = header.Value
4849
case ":scheme":
@@ -116,18 +117,14 @@ func ParseUserHeaders(headerNameToParseFrom string, headers []Header) ([]Header,
116117
}
117118

118119
func IsControlHeader(headerName string) bool {
119-
headerName = strings.ToLower(headerName)
120-
121120
return headerName == "content-length" ||
122121
headerName == "connection" || headerName == "upgrade" || // Websocket headers
123122
strings.HasPrefix(headerName, ":") ||
124123
strings.HasPrefix(headerName, "cf-")
125124
}
126125

127-
// IsWebsocketClientHeader returns true if the header name is required by the client to upgrade properly
128-
func IsWebsocketClientHeader(headerName string) bool {
129-
headerName = strings.ToLower(headerName)
130-
126+
// isWebsocketClientHeader returns true if the header name is required by the client to upgrade properly
127+
func isWebsocketClientHeader(headerName string) bool {
131128
return headerName == "sec-websocket-accept" ||
132129
headerName == "connection" ||
133130
headerName == "upgrade"
@@ -137,56 +134,73 @@ func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) {
137134
h2 = []Header{
138135
{Name: ":status", Value: strconv.Itoa(h1.StatusCode)},
139136
}
140-
userHeaders := http.Header{}
137+
userHeaders := make(http.Header, len(h1.Header))
141138
for header, values := range h1.Header {
142-
for _, value := range values {
143-
if strings.ToLower(header) == "content-length" {
144-
// This header has meaning in HTTP/2 and will be used by the edge,
145-
// so it should be sent as an HTTP/2 response header.
146-
147-
// Since these are http2 headers, they're required to be lowercase
148-
h2 = append(h2, Header{Name: strings.ToLower(header), Value: value})
149-
} else if !IsControlHeader(header) || IsWebsocketClientHeader(header) {
150-
// User headers, on the other hand, must all be serialized so that
151-
// HTTP/2 header validation won't be applied to HTTP/1 header values
152-
if _, ok := userHeaders[header]; ok {
153-
userHeaders[header] = append(userHeaders[header], value)
154-
} else {
155-
userHeaders[header] = []string{value}
156-
}
157-
}
139+
h2name := strings.ToLower(header)
140+
if h2name == "content-length" {
141+
// This header has meaning in HTTP/2 and will be used by the edge,
142+
// so it should be sent as an HTTP/2 response header.
143+
144+
// Since these are http2 headers, they're required to be lowercase
145+
h2 = append(h2, Header{Name: "content-length", Value: values[0]})
146+
} else if !IsControlHeader(h2name) || isWebsocketClientHeader(h2name) {
147+
// User headers, on the other hand, must all be serialized so that
148+
// HTTP/2 header validation won't be applied to HTTP/1 header values
149+
userHeaders[header] = values
158150
}
159151
}
160152

161153
// Perform user header serialization and set them in the single header
162-
h2 = append(h2, CreateSerializedHeaders(ResponseUserHeadersField, userHeaders)...)
154+
h2 = append(h2, Header{ResponseUserHeadersField, SerializeHeaders(userHeaders)})
163155

164156
return h2
165157
}
166158

167159
// Serialize HTTP1.x headers by base64-encoding each header name and value,
168160
// and then joining them in the format of [key:value;]
169161
func SerializeHeaders(h1Headers http.Header) string {
170-
var serializedHeaders []string
162+
// compute size of the fully serialized value and largest temp buffer we will need
163+
serializedLen := 0
164+
maxTempLen := 0
171165
for headerName, headerValues := range h1Headers {
172166
for _, headerValue := range headerValues {
173-
encodedName := make([]byte, headerEncoding.EncodedLen(len(headerName)))
174-
headerEncoding.Encode(encodedName, []byte(headerName))
175-
176-
encodedValue := make([]byte, headerEncoding.EncodedLen(len(headerValue)))
177-
headerEncoding.Encode(encodedValue, []byte(headerValue))
178-
179-
serializedHeaders = append(
180-
serializedHeaders,
181-
strings.Join(
182-
[]string{string(encodedName), string(encodedValue)},
183-
":",
184-
),
185-
)
167+
nameLen := headerEncoding.EncodedLen(len(headerName))
168+
valueLen := headerEncoding.EncodedLen(len(headerValue))
169+
const delims = 2
170+
serializedLen += delims + nameLen + valueLen
171+
if nameLen > maxTempLen {
172+
maxTempLen = nameLen
173+
}
174+
if valueLen > maxTempLen {
175+
maxTempLen = valueLen
176+
}
186177
}
187178
}
179+
var buf strings.Builder
180+
buf.Grow(serializedLen)
181+
182+
temp := make([]byte, maxTempLen)
183+
writeB64 := func(s string) {
184+
n := headerEncoding.EncodedLen(len(s))
185+
if n > len(temp) {
186+
temp = make([]byte, n)
187+
}
188+
headerEncoding.Encode(temp[:n], []byte(s))
189+
buf.Write(temp[:n])
190+
}
188191

189-
return strings.Join(serializedHeaders, ";")
192+
for headerName, headerValues := range h1Headers {
193+
for _, headerValue := range headerValues {
194+
if buf.Len() > 0 {
195+
buf.WriteByte(';')
196+
}
197+
writeB64(headerName)
198+
buf.WriteByte(':')
199+
writeB64(headerValue)
200+
}
201+
}
202+
203+
return buf.String()
190204
}
191205

192206
// Deserialize headers serialized by `SerializeHeader`
@@ -225,18 +239,6 @@ func DeserializeHeaders(serializedHeaders string) ([]Header, error) {
225239
return deserialized, nil
226240
}
227241

228-
func CreateSerializedHeaders(headersField string, headers ...http.Header) []Header {
229-
var serializedHeaderChunks []string
230-
for _, headerChunk := range headers {
231-
serializedHeaderChunks = append(serializedHeaderChunks, SerializeHeaders(headerChunk))
232-
}
233-
234-
return []Header{{
235-
headersField,
236-
strings.Join(serializedHeaderChunks, ";"),
237-
}}
238-
}
239-
240242
type ResponseMetaHeader struct {
241243
Source string `json:"src"`
242244
}

h2mux/header_test.go

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,19 @@ func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) {
3737
"Mock header 2": {"Mock value 2"},
3838
}
3939

40-
headersConversionErr := H2RequestHeadersToH1Request(CreateSerializedHeaders(RequestUserHeadersField, mockHeaders), request)
40+
headersConversionErr := H2RequestHeadersToH1Request(createSerializedHeaders(RequestUserHeadersField, mockHeaders), request)
4141

4242
assert.True(t, reflect.DeepEqual(mockHeaders, request.Header))
4343
assert.NoError(t, headersConversionErr)
4444
}
4545

46+
func createSerializedHeaders(headersField string, headers http.Header) []Header {
47+
return []Header{{
48+
headersField,
49+
SerializeHeaders(headers),
50+
}}
51+
}
52+
4653
func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) {
4754
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
4855
assert.NoError(t, err)
@@ -509,40 +516,32 @@ func TestParseHeaders(t *testing.T) {
509516
}
510517

511518
mockHeaders := []Header{
512-
{Name: "One", Value: "1"},
519+
{Name: "One", Value: "1"}, // will be dropped
513520
{Name: "Cf-Two", Value: "cf-value-1"},
514521
{Name: "Cf-Two", Value: "cf-value-2"},
515522
{Name: RequestUserHeadersField, Value: SerializeHeaders(mockUserHeadersToSerialize)},
516523
}
517524

518525
expectedHeaders := []Header{
526+
{Name: "Cf-Two", Value: "cf-value-1"},
527+
{Name: "Cf-Two", Value: "cf-value-2"},
519528
{Name: "Mock-Header-One", Value: "1"},
520529
{Name: "Mock-Header-One", Value: "1.5"},
521530
{Name: "Mock-Header-Two", Value: "2"},
522531
{Name: "Mock-Header-Three", Value: "3"},
523532
}
524-
parsedHeaders, err := ParseUserHeaders(RequestUserHeadersField, mockHeaders)
525-
assert.NoError(t, err)
526-
assert.ElementsMatch(t, expectedHeaders, parsedHeaders)
527-
}
528-
529-
func TestParseHeadersNoSerializedHeader(t *testing.T) {
530-
mockHeaders := []Header{
531-
{Name: "One", Value: "1"},
532-
{Name: "Cf-Two", Value: "cf-value-1"},
533-
{Name: "Cf-Two", Value: "cf-value-2"},
533+
h1 := &http.Request{
534+
Header: make(http.Header),
534535
}
535-
536-
_, err := ParseUserHeaders(RequestUserHeadersField, mockHeaders)
537-
assert.EqualError(t, err, fmt.Sprintf("%s header not found", RequestUserHeadersField))
536+
err := H2RequestHeadersToH1Request(mockHeaders, h1)
537+
assert.NoError(t, err)
538+
assert.ElementsMatch(t, expectedHeaders, stdlibHeaderToH2muxHeader(h1.Header))
538539
}
539540

540541
func TestIsControlHeader(t *testing.T) {
541542
controlHeaders := []string{
542543
// Anything that begins with cf-
543544
"cf-sample-header",
544-
"CF-SAMPLE-HEADER",
545-
"Cf-Sample-Header",
546545

547546
// Any http2 pseudoheader
548547
":sample-pseudo-header",
@@ -559,8 +558,8 @@ func TestIsControlHeader(t *testing.T) {
559558

560559
func TestIsNotControlHeader(t *testing.T) {
561560
notControlHeaders := []string{
562-
"Mock-header",
563-
"Another-sample-header",
561+
"mock-header",
562+
"another-sample-header",
564563
}
565564

566565
for _, header := range notControlHeaders {

0 commit comments

Comments
 (0)