Skip to content

Commit ca1505b

Browse files
authored
Improve HTTP/2 headers enumerator performance (#24726)
1 parent da6aa6f commit ca1505b

File tree

3 files changed

+194
-88
lines changed

3 files changed

+194
-88
lines changed

src/Servers/Kestrel/Core/src/Internal/Http2/Http2HeaderEnumerator.cs

Lines changed: 42 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
1111
{
1212
internal sealed class Http2HeadersEnumerator : IEnumerator<KeyValuePair<string, string>>
1313
{
14-
private bool _isTrailers;
14+
private enum HeadersType : byte
15+
{
16+
Headers,
17+
Trailers,
18+
Untyped
19+
}
20+
private HeadersType _headersType;
1521
private HttpResponseHeaders.Enumerator _headersEnumerator;
1622
private HttpResponseTrailers.Enumerator _trailersEnumerator;
1723
private IEnumerator<KeyValuePair<string, StringValues>> _genericEnumerator;
1824
private StringValues.Enumerator _stringValuesEnumerator;
25+
private bool _hasMultipleValues;
1926
private KnownHeaderType _knownHeaderType;
2027

2128
public int HPackStaticTableId => GetResponseHeaderStaticTableId(_knownHeaderType);
@@ -29,136 +36,89 @@ public Http2HeadersEnumerator()
2936
public void Initialize(HttpResponseHeaders headers)
3037
{
3138
_headersEnumerator = headers.GetEnumerator();
32-
_trailersEnumerator = default;
33-
_genericEnumerator = null;
34-
_isTrailers = false;
35-
36-
_stringValuesEnumerator = default;
37-
Current = default;
38-
_knownHeaderType = default;
39+
_headersType = HeadersType.Headers;
40+
_hasMultipleValues = false;
3941
}
4042

4143
public void Initialize(HttpResponseTrailers headers)
4244
{
43-
_headersEnumerator = default;
4445
_trailersEnumerator = headers.GetEnumerator();
45-
_genericEnumerator = null;
46-
_isTrailers = true;
47-
48-
_stringValuesEnumerator = default;
49-
Current = default;
50-
_knownHeaderType = default;
46+
_headersType = HeadersType.Trailers;
47+
_hasMultipleValues = false;
5148
}
5249

5350
public void Initialize(IDictionary<string, StringValues> headers)
5451
{
55-
_headersEnumerator = default;
56-
_trailersEnumerator = default;
5752
_genericEnumerator = headers.GetEnumerator();
58-
_isTrailers = false;
59-
60-
_stringValuesEnumerator = default;
61-
Current = default;
62-
_knownHeaderType = default;
53+
_headersType = HeadersType.Untyped;
54+
_hasMultipleValues = false;
6355
}
6456

6557
public bool MoveNext()
6658
{
67-
if (MoveNextOnStringEnumerator())
59+
if (_hasMultipleValues && MoveNextOnStringEnumerator(Current.Key))
6860
{
6961
return true;
7062
}
7163

72-
if (!TryGetNextStringEnumerator(out _stringValuesEnumerator))
64+
if (_headersType == HeadersType.Headers)
7365
{
74-
return false;
66+
return _headersEnumerator.MoveNext()
67+
? SetCurrent(_headersEnumerator.Current.Key, _headersEnumerator.Current.Value, _headersEnumerator.CurrentKnownType)
68+
: false;
7569
}
76-
77-
return MoveNextOnStringEnumerator();
78-
}
79-
80-
private string GetCurrentKey()
81-
{
82-
if (_genericEnumerator != null)
83-
{
84-
return _genericEnumerator.Current.Key;
85-
}
86-
else if (_isTrailers)
70+
else if (_headersType == HeadersType.Trailers)
8771
{
88-
return _trailersEnumerator.Current.Key;
72+
return _trailersEnumerator.MoveNext()
73+
? SetCurrent(_trailersEnumerator.Current.Key, _trailersEnumerator.Current.Value, _trailersEnumerator.CurrentKnownType)
74+
: false;
8975
}
9076
else
9177
{
92-
return _headersEnumerator.Current.Key;
78+
return _genericEnumerator.MoveNext()
79+
? SetCurrent(_genericEnumerator.Current.Key, _genericEnumerator.Current.Value, default)
80+
: false;
9381
}
9482
}
9583

96-
private bool MoveNextOnStringEnumerator()
84+
private bool MoveNextOnStringEnumerator(string key)
9785
{
9886
var result = _stringValuesEnumerator.MoveNext();
99-
Current = result ? new KeyValuePair<string, string>(GetCurrentKey(), _stringValuesEnumerator.Current) : default;
87+
Current = result ? new KeyValuePair<string, string>(key, _stringValuesEnumerator.Current) : default;
10088
return result;
10189
}
10290

103-
private bool TryGetNextStringEnumerator(out StringValues.Enumerator enumerator)
91+
private bool SetCurrent(string name, StringValues value, KnownHeaderType knownHeaderType)
10492
{
105-
if (_genericEnumerator != null)
106-
{
107-
if (!_genericEnumerator.MoveNext())
108-
{
109-
enumerator = default;
110-
return false;
111-
}
112-
else
113-
{
114-
enumerator = _genericEnumerator.Current.Value.GetEnumerator();
115-
_knownHeaderType = default;
116-
return true;
117-
}
118-
}
119-
else if (_isTrailers)
93+
if (value.Count == 1)
12094
{
121-
if (!_trailersEnumerator.MoveNext())
122-
{
123-
enumerator = default;
124-
return false;
125-
}
126-
else
127-
{
128-
enumerator = _trailersEnumerator.Current.Value.GetEnumerator();
129-
_knownHeaderType = _trailersEnumerator.CurrentKnownType;
130-
return true;
131-
}
95+
Current = new KeyValuePair<string, string>(name, value.ToString());
96+
_knownHeaderType = knownHeaderType;
97+
_hasMultipleValues = false;
98+
return true;
13299
}
133100
else
134101
{
135-
if (!_headersEnumerator.MoveNext())
136-
{
137-
enumerator = default;
138-
return false;
139-
}
140-
else
141-
{
142-
enumerator = _headersEnumerator.Current.Value.GetEnumerator();
143-
_knownHeaderType = _headersEnumerator.CurrentKnownType;
144-
return true;
145-
}
102+
_stringValuesEnumerator = value.GetEnumerator();
103+
_hasMultipleValues = true;
104+
105+
return MoveNextOnStringEnumerator(name);
146106
}
147107
}
148108

149109
public void Reset()
150110
{
151-
if (_genericEnumerator != null)
111+
if (_headersType == HeadersType.Headers)
152112
{
153-
_genericEnumerator.Reset();
113+
_headersEnumerator.Reset();
154114
}
155-
else if (_isTrailers)
115+
else if (_headersType == HeadersType.Trailers)
156116
{
157117
_trailersEnumerator.Reset();
158118
}
159119
else
160120
{
161-
_headersEnumerator.Reset();
121+
_genericEnumerator.Reset();
162122
}
163123
_stringValuesEnumerator = default;
164124
_knownHeaderType = default;

src/Servers/Kestrel/Core/test/Http2HeadersEnumeratorTests.cs

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,18 @@ public void CanIterateOverResponseHeaders()
5050
[Fact]
5151
public void CanIterateOverResponseTrailers()
5252
{
53-
var responseHeaders = new HttpResponseTrailers
53+
var responseTrailers = new HttpResponseTrailers
5454
{
5555
ContentLength = 9,
5656
HeaderETag = "ETag!"
5757
};
58-
responseHeaders.Append("Name1", "Value1");
59-
responseHeaders.Append("Name2", "Value2-1");
60-
responseHeaders.Append("Name2", "Value2-2");
61-
responseHeaders.Append("Name3", "Value3");
58+
responseTrailers.Append("Name1", "Value1");
59+
responseTrailers.Append("Name2", "Value2-1");
60+
responseTrailers.Append("Name2", "Value2-2");
61+
responseTrailers.Append("Name3", "Value3");
6262

6363
var e = new Http2HeadersEnumerator();
64-
e.Initialize(responseHeaders);
64+
e.Initialize(responseTrailers);
6565

6666
var headers = GetNormalizedHeaders(e);
6767

@@ -75,6 +75,58 @@ public void CanIterateOverResponseTrailers()
7575
}, headers);
7676
}
7777

78+
[Fact]
79+
public void Initialize_ChangeHeadersSource_EnumeratorUsesNewSource()
80+
{
81+
var responseHeaders = new HttpResponseHeaders();
82+
responseHeaders.Append("Name1", "Value1");
83+
responseHeaders.Append("Name2", "Value2-1");
84+
responseHeaders.Append("Name2", "Value2-2");
85+
86+
var e = new Http2HeadersEnumerator();
87+
e.Initialize(responseHeaders);
88+
89+
Assert.True(e.MoveNext());
90+
Assert.Equal("Name1", e.Current.Key);
91+
Assert.Equal("Value1", e.Current.Value);
92+
93+
Assert.True(e.MoveNext());
94+
Assert.Equal("Name2", e.Current.Key);
95+
Assert.Equal("Value2-1", e.Current.Value);
96+
97+
Assert.True(e.MoveNext());
98+
Assert.Equal("Name2", e.Current.Key);
99+
Assert.Equal("Value2-2", e.Current.Value);
100+
101+
var responseTrailers = new HttpResponseTrailers
102+
{
103+
HeaderGrpcStatus = "1"
104+
};
105+
responseTrailers.Append("Name1", "Value1");
106+
responseTrailers.Append("Name2", "Value2-1");
107+
responseTrailers.Append("Name2", "Value2-2");
108+
109+
e.Initialize(responseTrailers);
110+
111+
Assert.True(e.MoveNext());
112+
Assert.Equal("Grpc-Status", e.Current.Key);
113+
Assert.Equal("1", e.Current.Value);
114+
115+
Assert.True(e.MoveNext());
116+
Assert.Equal("Name1", e.Current.Key);
117+
Assert.Equal("Value1", e.Current.Value);
118+
119+
Assert.True(e.MoveNext());
120+
Assert.Equal("Name2", e.Current.Key);
121+
Assert.Equal("Value2-1", e.Current.Value);
122+
123+
Assert.True(e.MoveNext());
124+
Assert.Equal("Name2", e.Current.Key);
125+
Assert.Equal("Value2-2", e.Current.Value);
126+
127+
Assert.False(e.MoveNext());
128+
}
129+
78130
private KeyValuePair<string, string>[] GetNormalizedHeaders(Http2HeadersEnumerator enumerator)
79131
{
80132
var headers = new List<KeyValuePair<string, string>>();
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using BenchmarkDotNet.Attributes;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
7+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
8+
using Microsoft.Extensions.Primitives;
9+
10+
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
11+
{
12+
public class Http2HeadersEnumeratorBenchmark
13+
{
14+
private Http2HeadersEnumerator _enumerator;
15+
private HttpResponseHeaders _knownSingleValueResponseHeaders;
16+
private HttpResponseHeaders _knownMultipleValueResponseHeaders;
17+
private HttpResponseHeaders _unknownSingleValueResponseHeaders;
18+
private HttpResponseHeaders _unknownMultipleValueResponseHeaders;
19+
20+
[GlobalSetup]
21+
public void GlobalSetup()
22+
{
23+
_knownSingleValueResponseHeaders = new HttpResponseHeaders
24+
{
25+
HeaderServer = "Value",
26+
HeaderDate = "Value",
27+
HeaderContentType = "Value",
28+
HeaderSetCookie = "Value"
29+
};
30+
31+
_knownMultipleValueResponseHeaders = new HttpResponseHeaders
32+
{
33+
HeaderServer = new StringValues(new[] { "One", "Two" }),
34+
HeaderDate = new StringValues(new[] { "One", "Two" }),
35+
HeaderContentType = new StringValues(new[] { "One", "Two" }),
36+
HeaderSetCookie = new StringValues(new[] { "One", "Two" })
37+
};
38+
39+
_unknownSingleValueResponseHeaders = new HttpResponseHeaders();
40+
_unknownSingleValueResponseHeaders.Append("One", "Value");
41+
_unknownSingleValueResponseHeaders.Append("Two", "Value");
42+
_unknownSingleValueResponseHeaders.Append("Three", "Value");
43+
_unknownSingleValueResponseHeaders.Append("Four", "Value");
44+
45+
_unknownMultipleValueResponseHeaders = new HttpResponseHeaders();
46+
_unknownMultipleValueResponseHeaders.Append("One", new StringValues(new[] { "One", "Two" }));
47+
_unknownMultipleValueResponseHeaders.Append("Two", new StringValues(new[] { "One", "Two" }));
48+
_unknownMultipleValueResponseHeaders.Append("Three", new StringValues(new[] { "One", "Two" }));
49+
_unknownMultipleValueResponseHeaders.Append("Four", new StringValues(new[] { "One", "Two" }));
50+
51+
_enumerator = new Http2HeadersEnumerator();
52+
}
53+
54+
[Benchmark]
55+
public void KnownSingleValueResponseHeaders()
56+
{
57+
_enumerator.Initialize(_knownSingleValueResponseHeaders);
58+
59+
if (_enumerator.MoveNext())
60+
{
61+
}
62+
}
63+
64+
[Benchmark]
65+
public void KnownMultipleValueResponseHeaders()
66+
{
67+
_enumerator.Initialize(_knownMultipleValueResponseHeaders);
68+
69+
if (_enumerator.MoveNext())
70+
{
71+
}
72+
}
73+
74+
[Benchmark]
75+
public void UnknownSingleValueResponseHeaders()
76+
{
77+
_enumerator.Initialize(_unknownSingleValueResponseHeaders);
78+
79+
if (_enumerator.MoveNext())
80+
{
81+
}
82+
}
83+
84+
[Benchmark]
85+
public void UnknownMultipleValueResponseHeaders()
86+
{
87+
_enumerator.Initialize(_unknownMultipleValueResponseHeaders);
88+
89+
if (_enumerator.MoveNext())
90+
{
91+
}
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)