Skip to content

Commit e4fbd59

Browse files
benaadamsdavidfowl
authored andcommitted
Reuse previous materialized strings (#8374)
1 parent 4d78c21 commit e4fbd59

25 files changed

+2491
-983
lines changed

src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp3.0.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ public KestrelServerOptions() { }
121121
public Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.SchedulingMode ApplicationSchedulingMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
122122
public System.IServiceProvider ApplicationServices { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
123123
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader ConfigurationLoader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
124+
public bool DisableStringReuse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
124125
public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerLimits Limits { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
125126
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure() { throw null; }
126127
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure(Microsoft.Extensions.Configuration.IConfiguration config) { throw null; }
@@ -251,6 +252,7 @@ public enum HttpVersion
251252
public partial interface IHttpHeadersHandler
252253
{
253254
void OnHeader(System.Span<byte> name, System.Span<byte> value);
255+
void OnHeadersComplete();
254256
}
255257
public partial interface IHttpParser<TRequestHandler> where TRequestHandler : Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpHeadersHandler, Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpRequestLineHandler
256258
{

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

Lines changed: 117 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ internal partial class Http1Connection : HttpProtocol, IRequestProcessor
1919
private const byte ByteAsterisk = (byte)'*';
2020
private const byte ByteForwardSlash = (byte)'/';
2121
private const string Asterisk = "*";
22+
private const string ForwardSlash = "/";
2223

2324
private readonly HttpConnectionContext _context;
2425
private readonly IHttpParser<Http1ParsingHandler> _parser;
@@ -268,16 +269,68 @@ private void OnOriginFormTarget(HttpMethod method, HttpVersion version, Span<byt
268269

269270
_requestTargetForm = HttpRequestTarget.OriginForm;
270271

272+
if (target.Length == 1)
273+
{
274+
// If target.Length == 1 it can only be a forward slash (e.g. home page)
275+
// and we know RawTarget and Path are the same and QueryString is Empty
276+
RawTarget = ForwardSlash;
277+
Path = ForwardSlash;
278+
QueryString = string.Empty;
279+
// Clear parsedData as we won't check it if we come via this path again,
280+
// an setting to null is fast as it doesn't need to use a GC write barrier.
281+
_parsedRawTarget = _parsedPath = _parsedQueryString = null;
282+
return;
283+
}
284+
271285
// URIs are always encoded/escaped to ASCII https://tools.ietf.org/html/rfc3986#page-11
272286
// Multibyte Internationalized Resource Identifiers (IRIs) are first converted to utf8;
273287
// then encoded/escaped to ASCII https://www.ietf.org/rfc/rfc3987.txt "Mapping of IRIs to URIs"
274288

275289
try
276290
{
291+
var disableStringReuse = ServerOptions.DisableStringReuse;
277292
// Read raw target before mutating memory.
278-
RawTarget = target.GetAsciiStringNonNullCharacters();
279-
QueryString = query.GetAsciiStringNonNullCharacters();
280-
Path = PathNormalizer.DecodePath(path, pathEncoded, RawTarget, query.Length);
293+
var previousValue = _parsedRawTarget;
294+
if (disableStringReuse ||
295+
previousValue == null || previousValue.Length != target.Length ||
296+
!StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, target))
297+
{
298+
// The previous string does not match what the bytes would convert to,
299+
// so we will need to generate a new string.
300+
RawTarget = _parsedRawTarget = target.GetAsciiStringNonNullCharacters();
301+
302+
previousValue = _parsedQueryString;
303+
if (disableStringReuse ||
304+
previousValue == null || previousValue.Length != query.Length ||
305+
!StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, query))
306+
{
307+
// The previous string does not match what the bytes would convert to,
308+
// so we will need to generate a new string.
309+
QueryString = _parsedQueryString = query.GetAsciiStringNonNullCharacters();
310+
}
311+
else
312+
{
313+
// Same as previous
314+
QueryString = _parsedQueryString;
315+
}
316+
317+
if (path.Length == 1)
318+
{
319+
// If path.Length == 1 it can only be a forward slash (e.g. home page)
320+
Path = _parsedPath = ForwardSlash;
321+
}
322+
else
323+
{
324+
Path = _parsedPath = PathNormalizer.DecodePath(path, pathEncoded, RawTarget, query.Length);
325+
}
326+
}
327+
else
328+
{
329+
// As RawTarget is the same we can reuse the previous parsed values.
330+
RawTarget = _parsedRawTarget;
331+
Path = _parsedPath;
332+
QueryString = _parsedQueryString;
333+
}
281334
}
282335
catch (InvalidOperationException)
283336
{
@@ -312,9 +365,27 @@ private void OnAuthorityFormTarget(HttpMethod method, Span<byte> target)
312365
//
313366
// Allowed characters in the 'host + port' section of authority.
314367
// See https://tools.ietf.org/html/rfc3986#section-3.2
315-
RawTarget = target.GetAsciiStringNonNullCharacters();
368+
369+
var previousValue = _parsedRawTarget;
370+
if (ServerOptions.DisableStringReuse ||
371+
previousValue == null || previousValue.Length != target.Length ||
372+
!StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, target))
373+
{
374+
// The previous string does not match what the bytes would convert to,
375+
// so we will need to generate a new string.
376+
RawTarget = _parsedRawTarget = target.GetAsciiStringNonNullCharacters();
377+
}
378+
else
379+
{
380+
// Reuse previous value
381+
RawTarget = _parsedRawTarget;
382+
}
383+
316384
Path = string.Empty;
317385
QueryString = string.Empty;
386+
// Clear parsedData for path and queryString as we won't check it if we come via this path again,
387+
// an setting to null is fast as it doesn't need to use a GC write barrier.
388+
_parsedPath = _parsedQueryString = null;
318389
}
319390

320391
private void OnAsteriskFormTarget(HttpMethod method)
@@ -331,6 +402,9 @@ private void OnAsteriskFormTarget(HttpMethod method)
331402
RawTarget = Asterisk;
332403
Path = string.Empty;
333404
QueryString = string.Empty;
405+
// Clear parsedData as we won't check it if we come via this path again,
406+
// an setting to null is fast as it doesn't need to use a GC write barrier.
407+
_parsedRawTarget = _parsedPath = _parsedQueryString = null;
334408
}
335409

336410
private void OnAbsoluteFormTarget(Span<byte> target, Span<byte> query)
@@ -346,21 +420,49 @@ private void OnAbsoluteFormTarget(Span<byte> target, Span<byte> query)
346420
// a server MUST accept the absolute-form in requests, even though
347421
// HTTP/1.1 clients will only send them in requests to proxies.
348422

349-
RawTarget = target.GetAsciiStringNonNullCharacters();
423+
var disableStringReuse = ServerOptions.DisableStringReuse;
424+
var previousValue = _parsedRawTarget;
425+
if (disableStringReuse ||
426+
previousValue == null || previousValue.Length != target.Length ||
427+
!StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, target))
428+
{
429+
// The previous string does not match what the bytes would convert to,
430+
// so we will need to generate a new string.
431+
RawTarget = _parsedRawTarget = target.GetAsciiStringNonNullCharacters();
350432

351-
// Validation of absolute URIs is slow, but clients
352-
// should not be sending this form anyways, so perf optimization
353-
// not high priority
433+
// Validation of absolute URIs is slow, but clients
434+
// should not be sending this form anyways, so perf optimization
435+
// not high priority
354436

355-
if (!Uri.TryCreate(RawTarget, UriKind.Absolute, out var uri))
437+
if (!Uri.TryCreate(RawTarget, UriKind.Absolute, out var uri))
438+
{
439+
ThrowRequestTargetRejected(target);
440+
}
441+
442+
_absoluteRequestTarget = uri;
443+
Path = _parsedPath = uri.LocalPath;
444+
// don't use uri.Query because we need the unescaped version
445+
previousValue = _parsedQueryString;
446+
if (disableStringReuse ||
447+
previousValue == null || previousValue.Length != query.Length ||
448+
!StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, query))
449+
{
450+
// The previous string does not match what the bytes would convert to,
451+
// so we will need to generate a new string.
452+
QueryString = _parsedQueryString = query.GetAsciiStringNonNullCharacters();
453+
}
454+
else
455+
{
456+
QueryString = _parsedQueryString;
457+
}
458+
}
459+
else
356460
{
357-
ThrowRequestTargetRejected(target);
461+
// As RawTarget is the same we can reuse the previous values.
462+
RawTarget = _parsedRawTarget;
463+
Path = _parsedPath;
464+
QueryString = _parsedQueryString;
358465
}
359-
360-
_absoluteRequestTarget = uri;
361-
Path = uri.LocalPath;
362-
// don't use uri.Query because we need the unescaped version
363-
QueryString = query.GetAsciiStringNonNullCharacters();
364466
}
365467

366468
internal void EnsureHostHeaderExists()

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
}

0 commit comments

Comments
 (0)