diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2HeaderEnumerator.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2HeaderEnumerator.cs index db21bfb0fd..6e890dcaf7 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2HeaderEnumerator.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2HeaderEnumerator.cs @@ -11,11 +11,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { internal sealed class Http2HeadersEnumerator : IEnumerator> { - private bool _isTrailers; + private enum HeadersType : byte + { + Headers, + Trailers, + Untyped + } + private HeadersType _headersType; private HttpResponseHeaders.Enumerator _headersEnumerator; private HttpResponseTrailers.Enumerator _trailersEnumerator; private IEnumerator> _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 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(GetCurrentKey(), _stringValuesEnumerator.Current) : default; + Current = result ? new KeyValuePair(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(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; diff --git a/src/Servers/Kestrel/Core/test/Http2HeadersEnumeratorTests.cs b/src/Servers/Kestrel/Core/test/Http2HeadersEnumeratorTests.cs index 8aed0c4498..fa6e94095a 100644 --- a/src/Servers/Kestrel/Core/test/Http2HeadersEnumeratorTests.cs +++ b/src/Servers/Kestrel/Core/test/Http2HeadersEnumeratorTests.cs @@ -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[] GetNormalizedHeaders(Http2HeadersEnumerator enumerator) { var headers = new List>(); diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2HeadersEnumeratorBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2HeadersEnumeratorBenchmark.cs new file mode 100644 index 0000000000..9bcf7e63c0 --- /dev/null +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2HeadersEnumeratorBenchmark.cs @@ -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()) + { + } + } + } +}