Skip to content

Reuse previous materialized strings #8374

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ public KestrelServerOptions() { }
public Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.SchedulingMode ApplicationSchedulingMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public System.IServiceProvider ApplicationServices { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader ConfigurationLoader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool DisableStringReuse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerLimits Limits { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure() { throw null; }
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure(Microsoft.Extensions.Configuration.IConfiguration config) { throw null; }
Expand Down Expand Up @@ -251,6 +252,7 @@ public enum HttpVersion
public partial interface IHttpHeadersHandler
{
void OnHeader(System.Span<byte> name, System.Span<byte> value);
void OnHeadersComplete();
}
public partial interface IHttpParser<TRequestHandler> where TRequestHandler : Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpHeadersHandler, Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpRequestLineHandler
{
Expand Down
132 changes: 117 additions & 15 deletions src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ internal partial class Http1Connection : HttpProtocol, IRequestProcessor
private const byte ByteAsterisk = (byte)'*';
private const byte ByteForwardSlash = (byte)'/';
private const string Asterisk = "*";
private const string ForwardSlash = "/";

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

_requestTargetForm = HttpRequestTarget.OriginForm;

if (target.Length == 1)
{
// If target.Length == 1 it can only be a forward slash (e.g. home page)
// and we know RawTarget and Path are the same and QueryString is Empty
RawTarget = ForwardSlash;
Path = ForwardSlash;
QueryString = string.Empty;
// Clear parsedData as we won't check it if we come via this path again,
// an setting to null is fast as it doesn't need to use a GC write barrier.
_parsedRawTarget = _parsedPath = _parsedQueryString = null;
return;
}

// URIs are always encoded/escaped to ASCII https://tools.ietf.org/html/rfc3986#page-11
// Multibyte Internationalized Resource Identifiers (IRIs) are first converted to utf8;
// then encoded/escaped to ASCII https://www.ietf.org/rfc/rfc3987.txt "Mapping of IRIs to URIs"

try
{
var disableStringReuse = ServerOptions.DisableStringReuse;
// Read raw target before mutating memory.
RawTarget = target.GetAsciiStringNonNullCharacters();
QueryString = query.GetAsciiStringNonNullCharacters();
Path = PathNormalizer.DecodePath(path, pathEncoded, RawTarget, query.Length);
var previousValue = _parsedRawTarget;
if (disableStringReuse ||
previousValue == null || previousValue.Length != target.Length ||
!StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, target))
{
// The previous string does not match what the bytes would convert to,
// so we will need to generate a new string.
RawTarget = _parsedRawTarget = target.GetAsciiStringNonNullCharacters();

previousValue = _parsedQueryString;
if (disableStringReuse ||
previousValue == null || previousValue.Length != query.Length ||
!StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, query))
{
// The previous string does not match what the bytes would convert to,
// so we will need to generate a new string.
QueryString = _parsedQueryString = query.GetAsciiStringNonNullCharacters();
}
else
{
// Same as previous
QueryString = _parsedQueryString;
}

if (path.Length == 1)
{
// If path.Length == 1 it can only be a forward slash (e.g. home page)
Path = _parsedPath = ForwardSlash;
}
else
{
Path = _parsedPath = PathNormalizer.DecodePath(path, pathEncoded, RawTarget, query.Length);
}
}
else
{
// As RawTarget is the same we can reuse the previous parsed values.
RawTarget = _parsedRawTarget;
Path = _parsedPath;
QueryString = _parsedQueryString;
}
}
catch (InvalidOperationException)
{
Expand Down Expand Up @@ -312,9 +365,27 @@ private void OnAuthorityFormTarget(HttpMethod method, Span<byte> target)
//
// Allowed characters in the 'host + port' section of authority.
// See https://tools.ietf.org/html/rfc3986#section-3.2
RawTarget = target.GetAsciiStringNonNullCharacters();

var previousValue = _parsedRawTarget;
if (ServerOptions.DisableStringReuse ||
previousValue == null || previousValue.Length != target.Length ||
!StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, target))
{
// The previous string does not match what the bytes would convert to,
// so we will need to generate a new string.
RawTarget = _parsedRawTarget = target.GetAsciiStringNonNullCharacters();
}
else
{
// Reuse previous value
RawTarget = _parsedRawTarget;
}

Path = string.Empty;
QueryString = string.Empty;
// Clear parsedData for path and queryString as we won't check it if we come via this path again,
// an setting to null is fast as it doesn't need to use a GC write barrier.
_parsedPath = _parsedQueryString = null;
}

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

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

RawTarget = target.GetAsciiStringNonNullCharacters();
var disableStringReuse = ServerOptions.DisableStringReuse;
var previousValue = _parsedRawTarget;
if (disableStringReuse ||
previousValue == null || previousValue.Length != target.Length ||
!StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, target))
{
// The previous string does not match what the bytes would convert to,
// so we will need to generate a new string.
RawTarget = _parsedRawTarget = target.GetAsciiStringNonNullCharacters();

// Validation of absolute URIs is slow, but clients
// should not be sending this form anyways, so perf optimization
// not high priority
// Validation of absolute URIs is slow, but clients
// should not be sending this form anyways, so perf optimization
// not high priority

if (!Uri.TryCreate(RawTarget, UriKind.Absolute, out var uri))
if (!Uri.TryCreate(RawTarget, UriKind.Absolute, out var uri))
{
ThrowRequestTargetRejected(target);
}

_absoluteRequestTarget = uri;
Path = _parsedPath = uri.LocalPath;
// don't use uri.Query because we need the unescaped version
previousValue = _parsedQueryString;
if (disableStringReuse ||
previousValue == null || previousValue.Length != query.Length ||
!StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, query))
{
// The previous string does not match what the bytes would convert to,
// so we will need to generate a new string.
QueryString = _parsedQueryString = query.GetAsciiStringNonNullCharacters();
}
else
{
QueryString = _parsedQueryString;
}
}
else
{
ThrowRequestTargetRejected(target);
// As RawTarget is the same we can reuse the previous values.
RawTarget = _parsedRawTarget;
Path = _parsedPath;
QueryString = _parsedQueryString;
}

_absoluteRequestTarget = uri;
Path = uri.LocalPath;
// don't use uri.Query because we need the unescaped version
QueryString = query.GetAsciiStringNonNullCharacters();
}

internal void EnsureHostHeaderExists()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public Http1ParsingHandler(Http1Connection connection)
public void OnHeader(Span<byte> name, Span<byte> value)
=> Connection.OnHeader(name, value);

public void OnHeadersComplete()
=> Connection.OnHeadersComplete();

public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
=> Connection.OnStartLine(method, version, target, path, query, customMethod, pathEncoded);
}
Expand Down
Loading