From 44e3be2c8856e7d096d6744290e0ad12bd5d81c5 Mon Sep 17 00:00:00 2001 From: Igor Postelnik Date: Fri, 24 Jul 2020 23:23:00 -0500 Subject: [PATCH] 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% --- h2mux/header.go | 102 ++++++++++++++++++++++--------------------- h2mux/header_test.go | 37 ++++++++-------- 2 files changed, 70 insertions(+), 69 deletions(-) diff --git a/h2mux/header.go b/h2mux/header.go index 58d418e2..13aa3d57 100644 --- a/h2mux/header.go +++ b/h2mux/header.go @@ -38,11 +38,12 @@ const ( // HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3 func H2RequestHeadersToH1Request(h2 []Header, h1 *http.Request) error { for _, header := range h2 { - if !IsControlHeader(header.Name) { + name := strings.ToLower(header.Name) + if !IsControlHeader(name) { continue } - switch strings.ToLower(header.Name) { + switch name { case ":method": h1.Method = header.Value case ":scheme": @@ -116,18 +117,14 @@ func ParseUserHeaders(headerNameToParseFrom string, headers []Header) ([]Header, } func IsControlHeader(headerName string) bool { - headerName = strings.ToLower(headerName) - return headerName == "content-length" || headerName == "connection" || headerName == "upgrade" || // Websocket headers strings.HasPrefix(headerName, ":") || strings.HasPrefix(headerName, "cf-") } -// IsWebsocketClientHeader returns true if the header name is required by the client to upgrade properly -func IsWebsocketClientHeader(headerName string) bool { - headerName = strings.ToLower(headerName) - +// isWebsocketClientHeader returns true if the header name is required by the client to upgrade properly +func isWebsocketClientHeader(headerName string) bool { return headerName == "sec-websocket-accept" || headerName == "connection" || headerName == "upgrade" @@ -137,29 +134,24 @@ func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) { h2 = []Header{ {Name: ":status", Value: strconv.Itoa(h1.StatusCode)}, } - userHeaders := http.Header{} + userHeaders := make(http.Header, len(h1.Header)) for header, values := range h1.Header { - for _, value := range values { - if strings.ToLower(header) == "content-length" { - // This header has meaning in HTTP/2 and will be used by the edge, - // so it should be sent as an HTTP/2 response header. + h2name := strings.ToLower(header) + if h2name == "content-length" { + // This header has meaning in HTTP/2 and will be used by the edge, + // so it should be sent as an HTTP/2 response header. - // Since these are http2 headers, they're required to be lowercase - h2 = append(h2, Header{Name: strings.ToLower(header), Value: value}) - } else if !IsControlHeader(header) || IsWebsocketClientHeader(header) { - // User headers, on the other hand, must all be serialized so that - // HTTP/2 header validation won't be applied to HTTP/1 header values - if _, ok := userHeaders[header]; ok { - userHeaders[header] = append(userHeaders[header], value) - } else { - userHeaders[header] = []string{value} - } - } + // Since these are http2 headers, they're required to be lowercase + h2 = append(h2, Header{Name: "content-length", Value: values[0]}) + } else if !IsControlHeader(h2name) || isWebsocketClientHeader(h2name) { + // User headers, on the other hand, must all be serialized so that + // HTTP/2 header validation won't be applied to HTTP/1 header values + userHeaders[header] = values } } // Perform user header serialization and set them in the single header - h2 = append(h2, CreateSerializedHeaders(ResponseUserHeadersField, userHeaders)...) + h2 = append(h2, Header{ResponseUserHeadersField, SerializeHeaders(userHeaders)}) return h2 } @@ -167,26 +159,48 @@ func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) { // Serialize HTTP1.x headers by base64-encoding each header name and value, // and then joining them in the format of [key:value;] func SerializeHeaders(h1Headers http.Header) string { - var serializedHeaders []string + // compute size of the fully serialized value and largest temp buffer we will need + serializedLen := 0 + maxTempLen := 0 for headerName, headerValues := range h1Headers { for _, headerValue := range headerValues { - encodedName := make([]byte, headerEncoding.EncodedLen(len(headerName))) - headerEncoding.Encode(encodedName, []byte(headerName)) + nameLen := headerEncoding.EncodedLen(len(headerName)) + valueLen := headerEncoding.EncodedLen(len(headerValue)) + const delims = 2 + serializedLen += delims + nameLen + valueLen + if nameLen > maxTempLen { + maxTempLen = nameLen + } + if valueLen > maxTempLen { + maxTempLen = valueLen + } + } + } + var buf strings.Builder + buf.Grow(serializedLen) - encodedValue := make([]byte, headerEncoding.EncodedLen(len(headerValue))) - headerEncoding.Encode(encodedValue, []byte(headerValue)) + temp := make([]byte, maxTempLen) + writeB64 := func(s string) { + n := headerEncoding.EncodedLen(len(s)) + if n > len(temp) { + temp = make([]byte, n) + } + headerEncoding.Encode(temp[:n], []byte(s)) + buf.Write(temp[:n]) + } - serializedHeaders = append( - serializedHeaders, - strings.Join( - []string{string(encodedName), string(encodedValue)}, - ":", - ), - ) + for headerName, headerValues := range h1Headers { + for _, headerValue := range headerValues { + if buf.Len() > 0 { + buf.WriteByte(';') + } + writeB64(headerName) + buf.WriteByte(':') + writeB64(headerValue) } } - return strings.Join(serializedHeaders, ";") + return buf.String() } // Deserialize headers serialized by `SerializeHeader` @@ -225,18 +239,6 @@ func DeserializeHeaders(serializedHeaders string) ([]Header, error) { return deserialized, nil } -func CreateSerializedHeaders(headersField string, headers ...http.Header) []Header { - var serializedHeaderChunks []string - for _, headerChunk := range headers { - serializedHeaderChunks = append(serializedHeaderChunks, SerializeHeaders(headerChunk)) - } - - return []Header{{ - headersField, - strings.Join(serializedHeaderChunks, ";"), - }} -} - type ResponseMetaHeader struct { Source string `json:"src"` } diff --git a/h2mux/header_test.go b/h2mux/header_test.go index 787d278f..6b2411b2 100644 --- a/h2mux/header_test.go +++ b/h2mux/header_test.go @@ -37,12 +37,19 @@ func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) { "Mock header 2": {"Mock value 2"}, } - headersConversionErr := H2RequestHeadersToH1Request(CreateSerializedHeaders(RequestUserHeadersField, mockHeaders), request) + headersConversionErr := H2RequestHeadersToH1Request(createSerializedHeaders(RequestUserHeadersField, mockHeaders), request) assert.True(t, reflect.DeepEqual(mockHeaders, request.Header)) assert.NoError(t, headersConversionErr) } +func createSerializedHeaders(headersField string, headers http.Header) []Header { + return []Header{{ + headersField, + SerializeHeaders(headers), + }} +} + func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) { request, err := http.NewRequest(http.MethodGet, "http://example.com", nil) assert.NoError(t, err) @@ -509,40 +516,32 @@ func TestParseHeaders(t *testing.T) { } mockHeaders := []Header{ - {Name: "One", Value: "1"}, + {Name: "One", Value: "1"}, // will be dropped {Name: "Cf-Two", Value: "cf-value-1"}, {Name: "Cf-Two", Value: "cf-value-2"}, {Name: RequestUserHeadersField, Value: SerializeHeaders(mockUserHeadersToSerialize)}, } expectedHeaders := []Header{ + {Name: "Cf-Two", Value: "cf-value-1"}, + {Name: "Cf-Two", Value: "cf-value-2"}, {Name: "Mock-Header-One", Value: "1"}, {Name: "Mock-Header-One", Value: "1.5"}, {Name: "Mock-Header-Two", Value: "2"}, {Name: "Mock-Header-Three", Value: "3"}, } - parsedHeaders, err := ParseUserHeaders(RequestUserHeadersField, mockHeaders) - assert.NoError(t, err) - assert.ElementsMatch(t, expectedHeaders, parsedHeaders) -} - -func TestParseHeadersNoSerializedHeader(t *testing.T) { - mockHeaders := []Header{ - {Name: "One", Value: "1"}, - {Name: "Cf-Two", Value: "cf-value-1"}, - {Name: "Cf-Two", Value: "cf-value-2"}, + h1 := &http.Request{ + Header: make(http.Header), } - - _, err := ParseUserHeaders(RequestUserHeadersField, mockHeaders) - assert.EqualError(t, err, fmt.Sprintf("%s header not found", RequestUserHeadersField)) + err := H2RequestHeadersToH1Request(mockHeaders, h1) + assert.NoError(t, err) + assert.ElementsMatch(t, expectedHeaders, stdlibHeaderToH2muxHeader(h1.Header)) } func TestIsControlHeader(t *testing.T) { controlHeaders := []string{ // Anything that begins with cf- "cf-sample-header", - "CF-SAMPLE-HEADER", - "Cf-Sample-Header", // Any http2 pseudoheader ":sample-pseudo-header", @@ -559,8 +558,8 @@ func TestIsControlHeader(t *testing.T) { func TestIsNotControlHeader(t *testing.T) { notControlHeaders := []string{ - "Mock-header", - "Another-sample-header", + "mock-header", + "another-sample-header", } for _, header := range notControlHeaders {