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%
This commit is contained in:
parent
61d5461138
commit
44e3be2c88
102
h2mux/header.go
102
h2mux/header.go
|
@ -38,11 +38,12 @@ const (
|
||||||
// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3
|
// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3
|
||||||
func H2RequestHeadersToH1Request(h2 []Header, h1 *http.Request) error {
|
func H2RequestHeadersToH1Request(h2 []Header, h1 *http.Request) error {
|
||||||
for _, header := range h2 {
|
for _, header := range h2 {
|
||||||
if !IsControlHeader(header.Name) {
|
name := strings.ToLower(header.Name)
|
||||||
|
if !IsControlHeader(name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
switch strings.ToLower(header.Name) {
|
switch name {
|
||||||
case ":method":
|
case ":method":
|
||||||
h1.Method = header.Value
|
h1.Method = header.Value
|
||||||
case ":scheme":
|
case ":scheme":
|
||||||
|
@ -116,18 +117,14 @@ func ParseUserHeaders(headerNameToParseFrom string, headers []Header) ([]Header,
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsControlHeader(headerName string) bool {
|
func IsControlHeader(headerName string) bool {
|
||||||
headerName = strings.ToLower(headerName)
|
|
||||||
|
|
||||||
return headerName == "content-length" ||
|
return headerName == "content-length" ||
|
||||||
headerName == "connection" || headerName == "upgrade" || // Websocket headers
|
headerName == "connection" || headerName == "upgrade" || // Websocket headers
|
||||||
strings.HasPrefix(headerName, ":") ||
|
strings.HasPrefix(headerName, ":") ||
|
||||||
strings.HasPrefix(headerName, "cf-")
|
strings.HasPrefix(headerName, "cf-")
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsWebsocketClientHeader returns true if the header name is required by the client to upgrade properly
|
// isWebsocketClientHeader returns true if the header name is required by the client to upgrade properly
|
||||||
func IsWebsocketClientHeader(headerName string) bool {
|
func isWebsocketClientHeader(headerName string) bool {
|
||||||
headerName = strings.ToLower(headerName)
|
|
||||||
|
|
||||||
return headerName == "sec-websocket-accept" ||
|
return headerName == "sec-websocket-accept" ||
|
||||||
headerName == "connection" ||
|
headerName == "connection" ||
|
||||||
headerName == "upgrade"
|
headerName == "upgrade"
|
||||||
|
@ -137,29 +134,24 @@ func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) {
|
||||||
h2 = []Header{
|
h2 = []Header{
|
||||||
{Name: ":status", Value: strconv.Itoa(h1.StatusCode)},
|
{Name: ":status", Value: strconv.Itoa(h1.StatusCode)},
|
||||||
}
|
}
|
||||||
userHeaders := http.Header{}
|
userHeaders := make(http.Header, len(h1.Header))
|
||||||
for header, values := range h1.Header {
|
for header, values := range h1.Header {
|
||||||
for _, value := range values {
|
h2name := strings.ToLower(header)
|
||||||
if strings.ToLower(header) == "content-length" {
|
if h2name == "content-length" {
|
||||||
// This header has meaning in HTTP/2 and will be used by the edge,
|
// 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.
|
// so it should be sent as an HTTP/2 response header.
|
||||||
|
|
||||||
// Since these are http2 headers, they're required to be lowercase
|
// Since these are http2 headers, they're required to be lowercase
|
||||||
h2 = append(h2, Header{Name: strings.ToLower(header), Value: value})
|
h2 = append(h2, Header{Name: "content-length", Value: values[0]})
|
||||||
} else if !IsControlHeader(header) || IsWebsocketClientHeader(header) {
|
} else if !IsControlHeader(h2name) || isWebsocketClientHeader(h2name) {
|
||||||
// User headers, on the other hand, must all be serialized so that
|
// 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
|
// HTTP/2 header validation won't be applied to HTTP/1 header values
|
||||||
if _, ok := userHeaders[header]; ok {
|
userHeaders[header] = values
|
||||||
userHeaders[header] = append(userHeaders[header], value)
|
|
||||||
} else {
|
|
||||||
userHeaders[header] = []string{value}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform user header serialization and set them in the single header
|
// 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
|
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,
|
// Serialize HTTP1.x headers by base64-encoding each header name and value,
|
||||||
// and then joining them in the format of [key:value;]
|
// and then joining them in the format of [key:value;]
|
||||||
func SerializeHeaders(h1Headers http.Header) string {
|
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 headerName, headerValues := range h1Headers {
|
||||||
for _, headerValue := range headerValues {
|
for _, headerValue := range headerValues {
|
||||||
encodedName := make([]byte, headerEncoding.EncodedLen(len(headerName)))
|
nameLen := headerEncoding.EncodedLen(len(headerName))
|
||||||
headerEncoding.Encode(encodedName, []byte(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)))
|
temp := make([]byte, maxTempLen)
|
||||||
headerEncoding.Encode(encodedValue, []byte(headerValue))
|
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(
|
for headerName, headerValues := range h1Headers {
|
||||||
serializedHeaders,
|
for _, headerValue := range headerValues {
|
||||||
strings.Join(
|
if buf.Len() > 0 {
|
||||||
[]string{string(encodedName), string(encodedValue)},
|
buf.WriteByte(';')
|
||||||
":",
|
}
|
||||||
),
|
writeB64(headerName)
|
||||||
)
|
buf.WriteByte(':')
|
||||||
|
writeB64(headerValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.Join(serializedHeaders, ";")
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deserialize headers serialized by `SerializeHeader`
|
// Deserialize headers serialized by `SerializeHeader`
|
||||||
|
@ -225,18 +239,6 @@ func DeserializeHeaders(serializedHeaders string) ([]Header, error) {
|
||||||
return deserialized, nil
|
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 {
|
type ResponseMetaHeader struct {
|
||||||
Source string `json:"src"`
|
Source string `json:"src"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,12 +37,19 @@ func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) {
|
||||||
"Mock header 2": {"Mock value 2"},
|
"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.True(t, reflect.DeepEqual(mockHeaders, request.Header))
|
||||||
assert.NoError(t, headersConversionErr)
|
assert.NoError(t, headersConversionErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createSerializedHeaders(headersField string, headers http.Header) []Header {
|
||||||
|
return []Header{{
|
||||||
|
headersField,
|
||||||
|
SerializeHeaders(headers),
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) {
|
func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) {
|
||||||
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -509,40 +516,32 @@ func TestParseHeaders(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
mockHeaders := []Header{
|
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-1"},
|
||||||
{Name: "Cf-Two", Value: "cf-value-2"},
|
{Name: "Cf-Two", Value: "cf-value-2"},
|
||||||
{Name: RequestUserHeadersField, Value: SerializeHeaders(mockUserHeadersToSerialize)},
|
{Name: RequestUserHeadersField, Value: SerializeHeaders(mockUserHeadersToSerialize)},
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedHeaders := []Header{
|
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"},
|
||||||
{Name: "Mock-Header-One", Value: "1.5"},
|
{Name: "Mock-Header-One", Value: "1.5"},
|
||||||
{Name: "Mock-Header-Two", Value: "2"},
|
{Name: "Mock-Header-Two", Value: "2"},
|
||||||
{Name: "Mock-Header-Three", Value: "3"},
|
{Name: "Mock-Header-Three", Value: "3"},
|
||||||
}
|
}
|
||||||
parsedHeaders, err := ParseUserHeaders(RequestUserHeadersField, mockHeaders)
|
h1 := &http.Request{
|
||||||
assert.NoError(t, err)
|
Header: make(http.Header),
|
||||||
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"},
|
|
||||||
}
|
}
|
||||||
|
err := H2RequestHeadersToH1Request(mockHeaders, h1)
|
||||||
_, err := ParseUserHeaders(RequestUserHeadersField, mockHeaders)
|
assert.NoError(t, err)
|
||||||
assert.EqualError(t, err, fmt.Sprintf("%s header not found", RequestUserHeadersField))
|
assert.ElementsMatch(t, expectedHeaders, stdlibHeaderToH2muxHeader(h1.Header))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsControlHeader(t *testing.T) {
|
func TestIsControlHeader(t *testing.T) {
|
||||||
controlHeaders := []string{
|
controlHeaders := []string{
|
||||||
// Anything that begins with cf-
|
// Anything that begins with cf-
|
||||||
"cf-sample-header",
|
"cf-sample-header",
|
||||||
"CF-SAMPLE-HEADER",
|
|
||||||
"Cf-Sample-Header",
|
|
||||||
|
|
||||||
// Any http2 pseudoheader
|
// Any http2 pseudoheader
|
||||||
":sample-pseudo-header",
|
":sample-pseudo-header",
|
||||||
|
@ -559,8 +558,8 @@ func TestIsControlHeader(t *testing.T) {
|
||||||
|
|
||||||
func TestIsNotControlHeader(t *testing.T) {
|
func TestIsNotControlHeader(t *testing.T) {
|
||||||
notControlHeaders := []string{
|
notControlHeaders := []string{
|
||||||
"Mock-header",
|
"mock-header",
|
||||||
"Another-sample-header",
|
"another-sample-header",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, header := range notControlHeaders {
|
for _, header := range notControlHeaders {
|
||||||
|
|
Loading…
Reference in New Issue