Skip to content

Commit 8460ce5

Browse files
committed
Reuse previous request header value strings
1 parent 1ac6f3d commit 8460ce5

File tree

11 files changed

+711
-798
lines changed

11 files changed

+711
-798
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ public Http1ParsingHandler(Http1Connection connection)
1717
public void OnHeader(Span<byte> name, Span<byte> value)
1818
=> Connection.OnHeader(name, value);
1919

20+
public void OnHeadersComplete()
21+
=> Connection.OnHeadersComplete();
22+
2023
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
2124
=> Connection.OnStartLine(method, version, target, path, query, customMethod, pathEncoded);
2225
}

src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs

Lines changed: 352 additions & 720 deletions
Large diffs are not rendered by default.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
1616
{
1717
public abstract class HttpHeaders : IHeaderDictionary
1818
{
19+
protected long _bits = 0;
1920
protected long? _contentLength;
2021
protected bool _isReadOnly;
2122
protected Dictionary<string, StringValues> MaybeUnknown;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ public unsafe bool ParseHeaders(TRequestHandler handler, in ReadOnlySequence<byt
241241
}
242242

243243
done = true;
244+
handler.OnHeadersComplete();
244245
return true;
245246
}
246247

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public HttpProtocol(HttpConnectionContext context)
7474
_context = context;
7575

7676
ServerOptions = ServiceContext.ServerOptions;
77+
HttpRequestHeaders = new HttpRequestHeaders(ServerOptions);
7778
HttpResponseControl = this;
7879
}
7980

@@ -275,7 +276,7 @@ public CancellationToken RequestAborted
275276

276277
public bool HasFlushedHeaders => _requestProcessingStatus == RequestProcessingStatus.HeadersFlushed;
277278

278-
protected HttpRequestHeaders HttpRequestHeaders { get; } = new HttpRequestHeaders();
279+
protected HttpRequestHeaders HttpRequestHeaders { get; }
279280

280281
protected HttpResponseHeaders HttpResponseHeaders { get; } = new HttpResponseHeaders();
281282

@@ -492,9 +493,13 @@ public void OnHeader(Span<byte> name, Span<byte> value)
492493
{
493494
BadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders);
494495
}
495-
var valueString = value.GetAsciiOrUTF8StringNonNullCharacters();
496496

497-
HttpRequestHeaders.Append(name, valueString);
497+
HttpRequestHeaders.Append(name, value);
498+
}
499+
500+
public void OnHeadersComplete()
501+
{
502+
HttpRequestHeaders.OnHeadersComplete();
498503
}
499504

500505
public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> application)

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

Lines changed: 99 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections;
66
using System.Collections.Generic;
7+
using System.Diagnostics;
78
using System.Runtime.CompilerServices;
89
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
910
using Microsoft.Extensions.Primitives;
@@ -13,6 +14,46 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
1314
{
1415
public sealed partial class HttpRequestHeaders : HttpHeaders
1516
{
17+
private readonly KestrelServerOptions _serverOptions;
18+
private long _previousBits = 0;
19+
20+
public HttpRequestHeaders(KestrelServerOptions serverOptions)
21+
{
22+
_serverOptions = serverOptions;
23+
}
24+
25+
public void OnHeadersComplete()
26+
{
27+
var bitsToClear = _previousBits & ~_bits;
28+
_previousBits = 0;
29+
30+
if (bitsToClear != 0)
31+
{
32+
// Some previous headers were not reused or overwritten, so clear them now.
33+
Clear(bitsToClear);
34+
}
35+
}
36+
37+
protected override void ClearFast()
38+
{
39+
if (!_serverOptions.ReuseRequestHeaders)
40+
{
41+
// If we aren't reusing headers clear them all
42+
Clear(_bits);
43+
}
44+
else
45+
{
46+
// If we are reusing headers, store the currently set headers for comparision later
47+
_previousBits = _bits;
48+
}
49+
50+
// Mark no headers as currently in use
51+
_bits = 0;
52+
// Clear ContentLength and any unknown headers as we will never reuse them
53+
_contentLength = null;
54+
MaybeUnknown?.Clear();
55+
}
56+
1657
private static long ParseContentLength(string value)
1758
{
1859
if (!HeaderUtilities.TryParseNonNegativeInt64(value, out var parsed))
@@ -24,33 +65,82 @@ private static long ParseContentLength(string value)
2465
}
2566

2667
[MethodImpl(MethodImplOptions.NoInlining)]
27-
private void SetValueUnknown(string key, in StringValues value)
68+
private void AppendContentLength(Span<byte> value)
2869
{
29-
Unknown[key] = value;
70+
if (_contentLength.HasValue)
71+
{
72+
BadHttpRequestException.Throw(RequestRejectionReason.MultipleContentLengths);
73+
}
74+
75+
76+
if (!TryParseNonNegativeInt64(value, out var parsed))
77+
{
78+
BadHttpRequestException.Throw(RequestRejectionReason.InvalidContentLength, value.GetAsciiOrUTF8StringNonNullCharacters());
79+
}
80+
81+
_contentLength = parsed;
3082
}
3183

32-
public unsafe void Append(Span<byte> name, string value)
84+
private static unsafe bool TryParseNonNegativeInt64(Span<byte> value, out long result)
3385
{
34-
fixed (byte* namePtr = name)
86+
if (value.Length == 0)
87+
{
88+
result = 0;
89+
return false;
90+
}
91+
92+
var calc = 0L;
93+
var i = 0;
94+
for (; i < value.Length; i++)
95+
{
96+
var digit = (long)value[i] - (long)0x30;
97+
if ((ulong)digit > 9)
98+
{
99+
// Not a digit
100+
break;
101+
}
102+
103+
calc = calc * 10 + digit;
104+
if (calc < 0)
105+
{
106+
// Overflow
107+
break;
108+
}
109+
}
110+
111+
if (i != value.Length)
35112
{
36-
Append(namePtr, name.Length, value);
113+
// Didn't parse correct until end
114+
result = 0;
115+
return false;
37116
}
117+
118+
result = calc;
119+
return true;
120+
}
121+
122+
[MethodImpl(MethodImplOptions.NoInlining)]
123+
private void SetValueUnknown(string key, in StringValues value)
124+
{
125+
Unknown[key] = value;
38126
}
39127

40128
[MethodImpl(MethodImplOptions.NoInlining)]
41-
private unsafe void AppendUnknownHeaders(byte* pKeyBytes, int keyLength, string value)
129+
private unsafe void AppendUnknownHeaders(Span<byte> name, Span<byte> value)
42130
{
43-
string key = new string('\0', keyLength);
131+
string key = new string('\0', name.Length);
132+
fixed (byte* pKeyBytes = name)
44133
fixed (char* keyBuffer = key)
45134
{
46-
if (!StringUtilities.TryGetAsciiString(pKeyBytes, keyBuffer, keyLength))
135+
if (!StringUtilities.TryGetAsciiString(pKeyBytes, keyBuffer, name.Length))
47136
{
48137
BadHttpRequestException.Throw(RequestRejectionReason.InvalidCharactersInHeaderName);
49138
}
50139
}
51140

141+
var valueString = value.GetAsciiOrUTF8StringNonNullCharacters();
52142
Unknown.TryGetValue(key, out var existing);
53-
Unknown[key] = AppendValue(existing, value);
143+
Unknown[key] = AppendValue(existing, valueString);
54144
}
55145

56146
public Enumerator GetEnumerator()

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
88
public interface IHttpHeadersHandler
99
{
1010
void OnHeader(Span<byte> name, Span<byte> value);
11+
void OnHeadersComplete();
1112
}
1213
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,6 +1092,9 @@ public void OnHeader(Span<byte> name, Span<byte> value)
10921092
}
10931093
}
10941094

1095+
public void OnHeadersComplete()
1096+
=> _currentHeadersStream.OnHeadersComplete();
1097+
10951098
private void ValidateHeader(Span<byte> name, Span<byte> value)
10961099
{
10971100
// http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.1

src/Servers/Kestrel/Core/src/Internal/Infrastructure/StringUtilities.cs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Diagnostics;
56
using System.Numerics;
67
using System.Runtime.CompilerServices;
8+
using System.Runtime.InteropServices;
79

810
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
911
{
1012
internal class StringUtilities
1113
{
14+
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
1215
public static unsafe bool TryGetAsciiString(byte* input, char* output, int count)
1316
{
1417
// Calculate end position
@@ -109,6 +112,127 @@ out Unsafe.AsRef<Vector<short>>(output),
109112
return isValid;
110113
}
111114

115+
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
116+
public unsafe static bool BytesOrdinalEqualsStringAndAscii(string previousValue, Span<byte> newValue)
117+
{
118+
// We just widen the bytes to char for comparision, if either the string or the bytes are not ascii
119+
// this will result in non-equality, so we don't need to specifically test for non-ascii.
120+
Debug.Assert(previousValue.Length == newValue.Length);
121+
122+
// Use IntPtr values rather than int, to avoid unnessary 32 -> 64 movs on 64-bit.
123+
// Unfortunately this means we also need to cast to byte* for comparisions as IntPtr doesn't
124+
// support operator comparisions (e.g. <=, >, etc).
125+
// Note: Pointer comparision is unsigned, so we use the compare pattern (offset + length <= count)
126+
// rather than (offset <= count - length) which we'd do with signed comparision to avoid overflow.
127+
var count = (IntPtr)newValue.Length;
128+
var offset = (IntPtr)0;
129+
130+
ref var bytes = ref MemoryMarshal.GetReference(newValue);
131+
ref var str = ref MemoryMarshal.GetReference(previousValue.AsSpan());
132+
133+
do
134+
{
135+
// If Vector not-accelerated or remaining less than vector size
136+
if (!Vector.IsHardwareAccelerated || (byte*)(offset + Vector<byte>.Count) > (byte*)count)
137+
{
138+
if (IntPtr.Size == 8) // Use Intrinsic switch for branch elimination
139+
{
140+
// 64-bit: Loop longs by default
141+
while ((byte*)(offset + sizeof(long)) <= (byte*)count)
142+
{
143+
if (Unsafe.Add(ref str, offset) != (char)Unsafe.Add(ref bytes, offset) ||
144+
Unsafe.Add(ref str, offset + 1) != (char)Unsafe.Add(ref bytes, offset + 1) ||
145+
Unsafe.Add(ref str, offset + 2) != (char)Unsafe.Add(ref bytes, offset + 2) ||
146+
Unsafe.Add(ref str, offset + 3) != (char)Unsafe.Add(ref bytes, offset + 3) ||
147+
Unsafe.Add(ref str, offset + 4) != (char)Unsafe.Add(ref bytes, offset + 4) ||
148+
Unsafe.Add(ref str, offset + 5) != (char)Unsafe.Add(ref bytes, offset + 5) ||
149+
Unsafe.Add(ref str, offset + 6) != (char)Unsafe.Add(ref bytes, offset + 6) ||
150+
Unsafe.Add(ref str, offset + 7) != (char)Unsafe.Add(ref bytes, offset + 7))
151+
{
152+
goto NotEqual;
153+
}
154+
155+
offset += sizeof(long);
156+
}
157+
if ((byte*)(offset + sizeof(int)) <= (byte*)count)
158+
{
159+
if (Unsafe.Add(ref str, offset) != (char)Unsafe.Add(ref bytes, offset) ||
160+
Unsafe.Add(ref str, offset + 1) != (char)Unsafe.Add(ref bytes, offset + 1) ||
161+
Unsafe.Add(ref str, offset + 2) != (char)Unsafe.Add(ref bytes, offset + 2) ||
162+
Unsafe.Add(ref str, offset + 3) != (char)Unsafe.Add(ref bytes, offset + 3))
163+
{
164+
goto NotEqual;
165+
}
166+
167+
offset += sizeof(int);
168+
}
169+
}
170+
else
171+
{
172+
// 32-bit: Loop ints by default
173+
while ((byte*)(offset + sizeof(int)) <= (byte*)count)
174+
{
175+
if (Unsafe.Add(ref str, offset) != (char)Unsafe.Add(ref bytes, offset) ||
176+
Unsafe.Add(ref str, offset + 1) != (char)Unsafe.Add(ref bytes, offset + 1) ||
177+
Unsafe.Add(ref str, offset + 2) != (char)Unsafe.Add(ref bytes, offset + 2) ||
178+
Unsafe.Add(ref str, offset + 3) != (char)Unsafe.Add(ref bytes, offset + 3))
179+
{
180+
goto NotEqual;
181+
}
182+
183+
offset += sizeof(int);
184+
}
185+
}
186+
if ((byte*)(offset + sizeof(short)) <= (byte*)count)
187+
{
188+
if (Unsafe.Add(ref str, offset) != (char)Unsafe.Add(ref bytes, offset) ||
189+
Unsafe.Add(ref str, offset + 1) != (char)Unsafe.Add(ref bytes, offset + 1))
190+
{
191+
goto NotEqual;
192+
}
193+
194+
offset += sizeof(short);
195+
}
196+
if ((byte*)offset < (byte*)count)
197+
{
198+
if (Unsafe.Add(ref str, offset) != (char)Unsafe.Add(ref bytes, offset))
199+
{
200+
goto NotEqual;
201+
}
202+
}
203+
204+
return true;
205+
}
206+
207+
// do/while as entry condition already checked
208+
var AllTrue = new Vector<ushort>(ushort.MaxValue);
209+
do
210+
{
211+
var vector = Unsafe.As<byte, Vector<byte>>(ref Unsafe.Add(ref bytes, offset));
212+
Vector.Widen(vector, out var vector0, out var vector1);
213+
var compare0 = Unsafe.As<char, Vector<ushort>>(ref Unsafe.Add(ref str, offset));
214+
var compare1 = Unsafe.As<char, Vector<ushort>>(ref Unsafe.Add(ref str, offset + Vector<ushort>.Count));
215+
216+
if (!AllTrue.Equals(
217+
Vector.BitwiseAnd(
218+
Vector.Equals(compare0, vector0),
219+
Vector.Equals(compare1, vector1))))
220+
{
221+
goto NotEqual;
222+
}
223+
224+
offset += Vector<byte>.Count;
225+
} while ((byte*)(offset + Vector<byte>.Count) <= (byte*)count);
226+
227+
// Vector path done, loop back to do non-Vector
228+
// If is a exact multiple of vector size, bail now
229+
} while ((byte*)offset < (byte*)count);
230+
231+
return true;
232+
NotEqual:
233+
return false;
234+
}
235+
112236
private static readonly char[] s_encode16Chars = "0123456789ABCDEF".ToCharArray();
113237

114238
/// <summary>

src/Servers/Kestrel/Core/src/KestrelServerOptions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ public class KestrelServerOptions
5454
/// </remarks>
5555
public bool AllowSynchronousIO { get; set; } = false;
5656

57+
/// <summary>
58+
/// Gets or sets a value that controls whether the header values materialized as strings will be reused across requests;
59+
/// if they match, or if the strings will always be reallocated.
60+
/// </summary>
61+
/// <remarks>
62+
/// Defaults to false.
63+
/// </remarks>
64+
public bool ReuseRequestHeaders { get; set; } = true;
65+
5766
/// <summary>
5867
/// Enables the Listen options callback to resolve and use services registered by the application during startup.
5968
/// Typically initialized by UseKestrel()"/>.

0 commit comments

Comments
 (0)