Skip to content

Commit ec01a2a

Browse files
authored
Reduce HttpRequestHeader enumerator allocations (#32236)
1 parent 0b9dfa6 commit ec01a2a

File tree

2 files changed

+126
-3
lines changed

2 files changed

+126
-3
lines changed

src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
1616
{
1717
internal sealed partial class HttpRequestHeaders : HttpHeaders
1818
{
19+
private EnumeratorCache? _enumeratorCache;
1920
private long _previousBits;
2021

2122
public bool ReuseHeaderValues { get; set; }
@@ -65,6 +66,7 @@ protected override void ClearFast()
6566
// Clear ContentLength and any unknown headers as we will never reuse them
6667
_contentLength = null;
6768
MaybeUnknown?.Clear();
69+
_enumeratorCache?.Reset();
6870
}
6971

7072
private static long ParseContentLength(string value)
@@ -148,7 +150,73 @@ public Enumerator GetEnumerator()
148150

149151
protected override IEnumerator<KeyValuePair<string, StringValues>> GetEnumeratorFast()
150152
{
151-
return GetEnumerator();
153+
// Get or create the cache.
154+
var cache = _enumeratorCache ??= new();
155+
156+
EnumeratorBox enumerator;
157+
if (cache.CachedEnumerator is not null)
158+
{
159+
// Previous enumerator, reuse that one.
160+
enumerator = cache.InUseEnumerator = cache.CachedEnumerator;
161+
// Set previous to null so if there is a second enumerator call
162+
// during the same request it doesn't get the same one.
163+
cache.CachedEnumerator = null;
164+
}
165+
else
166+
{
167+
// Create new enumerator box and store as in use.
168+
enumerator = cache.InUseEnumerator = new();
169+
}
170+
171+
// Set the underlying struct enumerator to a new one.
172+
enumerator.Enumerator = new Enumerator(this);
173+
return enumerator;
174+
}
175+
176+
private class EnumeratorCache
177+
{
178+
/// <summary>
179+
/// Enumerator created from previous request
180+
/// </summary>
181+
public EnumeratorBox? CachedEnumerator { get; set; }
182+
/// <summary>
183+
/// Enumerator used on this request
184+
/// </summary>
185+
public EnumeratorBox? InUseEnumerator { get; set; }
186+
187+
/// <summary>
188+
/// Moves InUseEnumerator to CachedEnumerator
189+
/// </summary>
190+
public void Reset()
191+
{
192+
var enumerator = InUseEnumerator;
193+
if (enumerator is not null)
194+
{
195+
InUseEnumerator = null;
196+
enumerator.Enumerator = default;
197+
CachedEnumerator = enumerator;
198+
}
199+
}
200+
}
201+
202+
/// <summary>
203+
/// Strong box enumerator for the IEnumerator interface to cache and amortizate the
204+
/// IEnumerator allocations across requests if the header collection is commonly
205+
/// enumerated for forwarding in a reverse-proxy type situation.
206+
/// </summary>
207+
private class EnumeratorBox : IEnumerator<KeyValuePair<string, StringValues>>
208+
{
209+
public Enumerator Enumerator;
210+
211+
public KeyValuePair<string, StringValues> Current => Enumerator.Current;
212+
213+
public bool MoveNext() => Enumerator.MoveNext();
214+
215+
object IEnumerator.Current => Current;
216+
217+
public void Dispose() { }
218+
219+
public void Reset() => throw new NotSupportedException();
152220
}
153221

154222
public partial struct Enumerator : IEnumerator<KeyValuePair<string, StringValues>>

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

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,64 @@ public void SameExceptionThrownForMissingKey()
9999
}
100100

101101
[Fact]
102-
public void EntriesCanBeEnumerated()
102+
public void EntriesCanBeEnumeratedAfterResets()
103+
{
104+
HttpRequestHeaders headers = new HttpRequestHeaders();
105+
106+
EnumerateEntries((IHeaderDictionary)headers);
107+
headers.Reset();
108+
EnumerateEntries((IDictionary<string, StringValues>)headers);
109+
headers.Reset();
110+
EnumerateEntries((IHeaderDictionary)headers);
111+
headers.Reset();
112+
EnumerateEntries((IDictionary<string, StringValues>)headers);
113+
}
114+
115+
[Fact]
116+
public void EnumeratorNotReusedBeforeReset()
117+
{
118+
HttpRequestHeaders headers = new HttpRequestHeaders();
119+
IEnumerable<KeyValuePair<string, StringValues>> enumerable = headers;
120+
121+
var enumerator0 = enumerable.GetEnumerator();
122+
var enumerator1 = enumerable.GetEnumerator();
123+
124+
Assert.NotSame(enumerator0, enumerator1);
125+
}
126+
127+
[Fact]
128+
public void EnumeratorReusedAfterReset()
129+
{
130+
HttpRequestHeaders headers = new HttpRequestHeaders();
131+
IEnumerable<KeyValuePair<string, StringValues>> enumerable = headers;
132+
133+
var enumerator0 = enumerable.GetEnumerator();
134+
headers.Reset();
135+
var enumerator1 = enumerable.GetEnumerator();
136+
137+
Assert.Same(enumerator0, enumerator1);
138+
}
139+
140+
private static void EnumerateEntries(IHeaderDictionary headers)
141+
{
142+
var v1 = new[] { "localhost" };
143+
var v2 = new[] { "0" };
144+
var v3 = new[] { "value" };
145+
headers.Host = v1;
146+
headers.ContentLength = 0;
147+
headers["custom"] = v3;
148+
149+
Assert.Equal(
150+
new[] {
151+
new KeyValuePair<string, StringValues>("Host", v1),
152+
new KeyValuePair<string, StringValues>("Content-Length", v2),
153+
new KeyValuePair<string, StringValues>("custom", v3),
154+
},
155+
headers);
156+
}
157+
158+
private static void EnumerateEntries(IDictionary<string, StringValues> headers)
103159
{
104-
IDictionary<string, StringValues> headers = new HttpRequestHeaders();
105160
var v1 = new[] { "localhost" };
106161
var v2 = new[] { "0" };
107162
var v3 = new[] { "value" };

0 commit comments

Comments
 (0)