Improve HTTP/2 headers enumerator performance (#24726)

This commit is contained in:
James Newton-King 2020-08-13 15:28:18 +12:00 committed by GitHub
parent da6aa6fc6a
commit ca1505ba3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 194 additions and 88 deletions

View File

@ -11,11 +11,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
internal sealed class Http2HeadersEnumerator : IEnumerator<KeyValuePair<string, string>>
{
private bool _isTrailers;
private enum HeadersType : byte
{
Headers,
Trailers,
Untyped
}
private HeadersType _headersType;
private HttpResponseHeaders.Enumerator _headersEnumerator;
private HttpResponseTrailers.Enumerator _trailersEnumerator;
private IEnumerator<KeyValuePair<string, StringValues>> _genericEnumerator;
private StringValues.Enumerator _stringValuesEnumerator;
private bool _hasMultipleValues;
private KnownHeaderType _knownHeaderType;
public int HPackStaticTableId => GetResponseHeaderStaticTableId(_knownHeaderType);
@ -29,136 +36,89 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
public void Initialize(HttpResponseHeaders headers)
{
_headersEnumerator = headers.GetEnumerator();
_trailersEnumerator = default;
_genericEnumerator = null;
_isTrailers = false;
_stringValuesEnumerator = default;
Current = default;
_knownHeaderType = default;
_headersType = HeadersType.Headers;
_hasMultipleValues = false;
}
public void Initialize(HttpResponseTrailers headers)
{
_headersEnumerator = default;
_trailersEnumerator = headers.GetEnumerator();
_genericEnumerator = null;
_isTrailers = true;
_stringValuesEnumerator = default;
Current = default;
_knownHeaderType = default;
_headersType = HeadersType.Trailers;
_hasMultipleValues = false;
}
public void Initialize(IDictionary<string, StringValues> headers)
{
_headersEnumerator = default;
_trailersEnumerator = default;
_genericEnumerator = headers.GetEnumerator();
_isTrailers = false;
_stringValuesEnumerator = default;
Current = default;
_knownHeaderType = default;
_headersType = HeadersType.Untyped;
_hasMultipleValues = false;
}
public bool MoveNext()
{
if (MoveNextOnStringEnumerator())
if (_hasMultipleValues && MoveNextOnStringEnumerator(Current.Key))
{
return true;
}
if (!TryGetNextStringEnumerator(out _stringValuesEnumerator))
if (_headersType == HeadersType.Headers)
{
return false;
return _headersEnumerator.MoveNext()
? SetCurrent(_headersEnumerator.Current.Key, _headersEnumerator.Current.Value, _headersEnumerator.CurrentKnownType)
: false;
}
return MoveNextOnStringEnumerator();
}
private string GetCurrentKey()
{
if (_genericEnumerator != null)
else if (_headersType == HeadersType.Trailers)
{
return _genericEnumerator.Current.Key;
}
else if (_isTrailers)
{
return _trailersEnumerator.Current.Key;
return _trailersEnumerator.MoveNext()
? SetCurrent(_trailersEnumerator.Current.Key, _trailersEnumerator.Current.Value, _trailersEnumerator.CurrentKnownType)
: false;
}
else
{
return _headersEnumerator.Current.Key;
return _genericEnumerator.MoveNext()
? SetCurrent(_genericEnumerator.Current.Key, _genericEnumerator.Current.Value, default)
: false;
}
}
private bool MoveNextOnStringEnumerator()
private bool MoveNextOnStringEnumerator(string key)
{
var result = _stringValuesEnumerator.MoveNext();
Current = result ? new KeyValuePair<string, string>(GetCurrentKey(), _stringValuesEnumerator.Current) : default;
Current = result ? new KeyValuePair<string, string>(key, _stringValuesEnumerator.Current) : default;
return result;
}
private bool TryGetNextStringEnumerator(out StringValues.Enumerator enumerator)
private bool SetCurrent(string name, StringValues value, KnownHeaderType knownHeaderType)
{
if (_genericEnumerator != null)
if (value.Count == 1)
{
if (!_genericEnumerator.MoveNext())
{
enumerator = default;
return false;
}
else
{
enumerator = _genericEnumerator.Current.Value.GetEnumerator();
_knownHeaderType = default;
return true;
}
}
else if (_isTrailers)
{
if (!_trailersEnumerator.MoveNext())
{
enumerator = default;
return false;
}
else
{
enumerator = _trailersEnumerator.Current.Value.GetEnumerator();
_knownHeaderType = _trailersEnumerator.CurrentKnownType;
return true;
}
Current = new KeyValuePair<string, string>(name, value.ToString());
_knownHeaderType = knownHeaderType;
_hasMultipleValues = false;
return true;
}
else
{
if (!_headersEnumerator.MoveNext())
{
enumerator = default;
return false;
}
else
{
enumerator = _headersEnumerator.Current.Value.GetEnumerator();
_knownHeaderType = _headersEnumerator.CurrentKnownType;
return true;
}
_stringValuesEnumerator = value.GetEnumerator();
_hasMultipleValues = true;
return MoveNextOnStringEnumerator(name);
}
}
public void Reset()
{
if (_genericEnumerator != null)
if (_headersType == HeadersType.Headers)
{
_genericEnumerator.Reset();
_headersEnumerator.Reset();
}
else if (_isTrailers)
else if (_headersType == HeadersType.Trailers)
{
_trailersEnumerator.Reset();
}
else
{
_headersEnumerator.Reset();
_genericEnumerator.Reset();
}
_stringValuesEnumerator = default;
_knownHeaderType = default;

View File

@ -50,18 +50,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Fact]
public void CanIterateOverResponseTrailers()
{
var responseHeaders = new HttpResponseTrailers
var responseTrailers = new HttpResponseTrailers
{
ContentLength = 9,
HeaderETag = "ETag!"
};
responseHeaders.Append("Name1", "Value1");
responseHeaders.Append("Name2", "Value2-1");
responseHeaders.Append("Name2", "Value2-2");
responseHeaders.Append("Name3", "Value3");
responseTrailers.Append("Name1", "Value1");
responseTrailers.Append("Name2", "Value2-1");
responseTrailers.Append("Name2", "Value2-2");
responseTrailers.Append("Name3", "Value3");
var e = new Http2HeadersEnumerator();
e.Initialize(responseHeaders);
e.Initialize(responseTrailers);
var headers = GetNormalizedHeaders(e);
@ -75,6 +75,58 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}, headers);
}
[Fact]
public void Initialize_ChangeHeadersSource_EnumeratorUsesNewSource()
{
var responseHeaders = new HttpResponseHeaders();
responseHeaders.Append("Name1", "Value1");
responseHeaders.Append("Name2", "Value2-1");
responseHeaders.Append("Name2", "Value2-2");
var e = new Http2HeadersEnumerator();
e.Initialize(responseHeaders);
Assert.True(e.MoveNext());
Assert.Equal("Name1", e.Current.Key);
Assert.Equal("Value1", e.Current.Value);
Assert.True(e.MoveNext());
Assert.Equal("Name2", e.Current.Key);
Assert.Equal("Value2-1", e.Current.Value);
Assert.True(e.MoveNext());
Assert.Equal("Name2", e.Current.Key);
Assert.Equal("Value2-2", e.Current.Value);
var responseTrailers = new HttpResponseTrailers
{
HeaderGrpcStatus = "1"
};
responseTrailers.Append("Name1", "Value1");
responseTrailers.Append("Name2", "Value2-1");
responseTrailers.Append("Name2", "Value2-2");
e.Initialize(responseTrailers);
Assert.True(e.MoveNext());
Assert.Equal("Grpc-Status", e.Current.Key);
Assert.Equal("1", e.Current.Value);
Assert.True(e.MoveNext());
Assert.Equal("Name1", e.Current.Key);
Assert.Equal("Value1", e.Current.Value);
Assert.True(e.MoveNext());
Assert.Equal("Name2", e.Current.Key);
Assert.Equal("Value2-1", e.Current.Value);
Assert.True(e.MoveNext());
Assert.Equal("Name2", e.Current.Key);
Assert.Equal("Value2-2", e.Current.Value);
Assert.False(e.MoveNext());
}
private KeyValuePair<string, string>[] GetNormalizedHeaders(Http2HeadersEnumerator enumerator)
{
var headers = new List<KeyValuePair<string, string>>();

View File

@ -0,0 +1,94 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
{
public class Http2HeadersEnumeratorBenchmark
{
private Http2HeadersEnumerator _enumerator;
private HttpResponseHeaders _knownSingleValueResponseHeaders;
private HttpResponseHeaders _knownMultipleValueResponseHeaders;
private HttpResponseHeaders _unknownSingleValueResponseHeaders;
private HttpResponseHeaders _unknownMultipleValueResponseHeaders;
[GlobalSetup]
public void GlobalSetup()
{
_knownSingleValueResponseHeaders = new HttpResponseHeaders
{
HeaderServer = "Value",
HeaderDate = "Value",
HeaderContentType = "Value",
HeaderSetCookie = "Value"
};
_knownMultipleValueResponseHeaders = new HttpResponseHeaders
{
HeaderServer = new StringValues(new[] { "One", "Two" }),
HeaderDate = new StringValues(new[] { "One", "Two" }),
HeaderContentType = new StringValues(new[] { "One", "Two" }),
HeaderSetCookie = new StringValues(new[] { "One", "Two" })
};
_unknownSingleValueResponseHeaders = new HttpResponseHeaders();
_unknownSingleValueResponseHeaders.Append("One", "Value");
_unknownSingleValueResponseHeaders.Append("Two", "Value");
_unknownSingleValueResponseHeaders.Append("Three", "Value");
_unknownSingleValueResponseHeaders.Append("Four", "Value");
_unknownMultipleValueResponseHeaders = new HttpResponseHeaders();
_unknownMultipleValueResponseHeaders.Append("One", new StringValues(new[] { "One", "Two" }));
_unknownMultipleValueResponseHeaders.Append("Two", new StringValues(new[] { "One", "Two" }));
_unknownMultipleValueResponseHeaders.Append("Three", new StringValues(new[] { "One", "Two" }));
_unknownMultipleValueResponseHeaders.Append("Four", new StringValues(new[] { "One", "Two" }));
_enumerator = new Http2HeadersEnumerator();
}
[Benchmark]
public void KnownSingleValueResponseHeaders()
{
_enumerator.Initialize(_knownSingleValueResponseHeaders);
if (_enumerator.MoveNext())
{
}
}
[Benchmark]
public void KnownMultipleValueResponseHeaders()
{
_enumerator.Initialize(_knownMultipleValueResponseHeaders);
if (_enumerator.MoveNext())
{
}
}
[Benchmark]
public void UnknownSingleValueResponseHeaders()
{
_enumerator.Initialize(_unknownSingleValueResponseHeaders);
if (_enumerator.MoveNext())
{
}
}
[Benchmark]
public void UnknownMultipleValueResponseHeaders()
{
_enumerator.Initialize(_unknownMultipleValueResponseHeaders);
if (_enumerator.MoveNext())
{
}
}
}
}