Skip to content

Commit

Permalink
Reduce allocations for Cookies. (#31258)
Browse files Browse the repository at this point in the history
  • Loading branch information
jkotalik committed Mar 26, 2021
1 parent 9252bbc commit ec0841d
Show file tree
Hide file tree
Showing 13 changed files with 299 additions and 203 deletions.
76 changes: 8 additions & 68 deletions src/Http/Headers/src/CookieHeaderParser.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Diagnostics.Contracts;
using Microsoft.Extensions.Primitives;

namespace Microsoft.Net.Http.Headers
Expand All @@ -13,83 +12,24 @@ internal CookieHeaderParser(bool supportsMultipleValues)
{
}

public override bool TryParseValue(StringSegment value, ref int index, out CookieHeaderValue? parsedValue)
public override bool TryParseValue(StringSegment value, ref int index, out CookieHeaderValue? cookieValue)
{
parsedValue = null;
cookieValue = null;

// If multiple values are supported (i.e. list of values), then accept an empty string: The header may
// be added multiple times to the request/response message. E.g.
// Accept: text/xml; q=1
// Accept:
// Accept: text/plain; q=0.2
if (StringSegment.IsNullOrEmpty(value) || (index == value.Length))
{
return SupportsMultipleValues;
}

var current = GetNextNonEmptyOrWhitespaceIndex(value, index, SupportsMultipleValues, out bool separatorFound);

if (separatorFound && !SupportsMultipleValues)
{
return false; // leading separators not allowed if we don't support multiple values.
}

if (current == value.Length)
{
if (SupportsMultipleValues)
{
index = current;
}
return SupportsMultipleValues;
}

if (!CookieHeaderValue.TryGetCookieLength(value, ref current, out var result))
if (!CookieHeaderParserShared.TryParseValue(value, ref index, SupportsMultipleValues, out var parsedName, out var parsedValue))
{
return false;
}

current = GetNextNonEmptyOrWhitespaceIndex(value, current, SupportsMultipleValues, out separatorFound);

// If we support multiple values and we've not reached the end of the string, then we must have a separator.
if ((separatorFound && !SupportsMultipleValues) || (!separatorFound && (current < value.Length)))
if (parsedName == null || parsedValue == null)
{
return false;
// Successfully parsed, but no values.
return true;
}

index = current;
parsedValue = result;
return true;
}

private static int GetNextNonEmptyOrWhitespaceIndex(StringSegment input, int startIndex, bool skipEmptyValues, out bool separatorFound)
{
Contract.Requires(startIndex <= input.Length); // it's OK if index == value.Length.

separatorFound = false;
var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex);

if ((current == input.Length) || (input[current] != ',' && input[current] != ';'))
{
return current;
}

// If we have a separator, skip the separator and all following whitespaces. If we support
// empty values, continue until the current character is neither a separator nor a whitespace.
separatorFound = true;
current++; // skip delimiter.
current = current + HttpRuleParser.GetWhitespaceLength(input, current);

if (skipEmptyValues)
{
// Most headers only split on ',', but cookies primarily split on ';'
while ((current < input.Length) && ((input[current] == ',') || (input[current] == ';')))
{
current++; // skip delimiter.
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
}
}
cookieValue = new CookieHeaderValue(parsedName.Value, parsedValue.Value);

return current;
return true;
}
}
}
117 changes: 1 addition & 116 deletions src/Http/Headers/src/CookieHeaderValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,121 +165,6 @@ public static bool TryParseStrictList(IList<string>? inputs, [NotNullWhen(true)]
return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
}

// name=value; name="value"
internal static bool TryGetCookieLength(StringSegment input, ref int offset, [NotNullWhen(true)] out CookieHeaderValue? parsedValue)
{
Contract.Requires(offset >= 0);

parsedValue = null;

if (StringSegment.IsNullOrEmpty(input) || (offset >= input.Length))
{
return false;
}

var result = new CookieHeaderValue();

// The caller should have already consumed any leading whitespace, commas, etc..

// Name=value;

// Name
var itemLength = HttpRuleParser.GetTokenLength(input, offset);
if (itemLength == 0)
{
return false;
}
result._name = input.Subsegment(offset, itemLength);
offset += itemLength;

// = (no spaces)
if (!ReadEqualsSign(input, ref offset))
{
return false;
}

// value or "quoted value"
// The value may be empty
result._value = GetCookieValue(input, ref offset);

parsedValue = result;
return true;
}

// cookie-value = *cookie-octet / ( DQUOTE* cookie-octet DQUOTE )
// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
// ; US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash
internal static StringSegment GetCookieValue(StringSegment input, ref int offset)
{
Contract.Requires(offset >= 0);
Contract.Ensures((Contract.Result<int>() >= 0) && (Contract.Result<int>() <= (input.Length - offset)));

var startIndex = offset;

if (offset >= input.Length)
{
return StringSegment.Empty;
}
var inQuotes = false;

if (input[offset] == '"')
{
inQuotes = true;
offset++;
}

while (offset < input.Length)
{
var c = input[offset];
if (!IsCookieValueChar(c))
{
break;
}

offset++;
}

if (inQuotes)
{
if (offset == input.Length || input[offset] != '"')
{
// Missing final quote
return StringSegment.Empty;
}
offset++;
}

int length = offset - startIndex;
if (offset > startIndex)
{
return input.Subsegment(startIndex, length);
}

return StringSegment.Empty;
}

private static bool ReadEqualsSign(StringSegment input, ref int offset)
{
// = (no spaces)
if (offset >= input.Length || input[offset] != '=')
{
return false;
}
offset++;
return true;
}

// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
// ; US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash
private static bool IsCookieValueChar(char c)
{
if (c < 0x21 || c > 0x7E)
{
return false;
}
return !(c == '"' || c == ',' || c == ';' || c == '\\');
}

internal static void CheckNameFormat(StringSegment name, string parameterName)
{
if (name == null)
Expand All @@ -301,7 +186,7 @@ internal static void CheckValueFormat(StringSegment value, string parameterName)
}

var offset = 0;
var result = GetCookieValue(value, ref offset);
var result = CookieHeaderParserShared.GetCookieValue(value, ref offset);
if (result.Length != value.Length)
{
throw new ArgumentException("Invalid cookie value: " + value, parameterName);
Expand Down
3 changes: 3 additions & 0 deletions src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

<ItemGroup>
<Reference Include="Microsoft.Extensions.Primitives" />
<Compile Include="..\..\Shared\CookieHeaderParserShared.cs" Link="CookieHeaderParserShared.cs" />
<Compile Include="..\..\Shared\HttpRuleParser.cs" />
<Compile Include="..\..\Shared\HttpParseResult.cs" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/Http/Headers/src/SetCookieHeaderValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ private static int GetSetCookieLength(StringSegment input, int startIndex, out S

// value or "quoted value"
// The value may be empty
result._value = CookieHeaderValue.GetCookieValue(input, ref offset);
result._value = CookieHeaderParserShared.GetCookieValue(input, ref offset);

// *(';' SP cookie-av)
while (offset < input.Length)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using BenchmarkDotNet.Attributes;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.Http
{
public class RequestCookieCollectionBenchmarks
{
private StringValues _cookie;

[IterationSetup]
public void Setup()
{
_cookie = ".AspNetCore.Cookies=CfDJ8BAklVa9EYREk8_ipRUUYJYhRsleKr485k18s_q5XD6vcRJ-DtowUuLCwwMiY728zRZ3rVFY3DEcXDAQUOTtg1e4tkSIVmYLX38Q6mqdFFyw-8dksclDywe9vnN84cEWvfV0wP3EgOsJGHaND7kTJ47gr7Pc1tLHWOm4Pe7Q1vrT9EkcTMr1Wts3aptBl3bdOLLqjmSdgk-OI7qG7uQGz1OGdnSer6-KLUPBcfXblzs4YCjvwu3bGnM42xLGtkZNIF8izPpyqKkIf7ec6O6LEHMp4gcq86PGHCXHn5NKuNSD";
}

[Benchmark]
public void Parse_TypicalCookie()
{
RequestCookieCollection.Parse(_cookie);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
Expand Down
2 changes: 1 addition & 1 deletion src/Http/Http/src/Features/RequestCookiesFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public IRequestCookieCollection Cookies
if (_parsedValues == null || _original != current)
{
_original = current;
_parsedValues = RequestCookieCollection.Parse(current.ToArray());
_parsedValues = RequestCookieCollection.Parse(current);
}

return _parsedValues;
Expand Down
23 changes: 8 additions & 15 deletions src/Http/Http/src/Internal/RequestCookieCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNetCore.Http
Expand Down Expand Up @@ -56,33 +57,25 @@ public string? this[string key]
}
}

public static RequestCookieCollection Parse(IList<string> values)
=> ParseInternal(values, AppContext.TryGetSwitch(ResponseCookies.EnableCookieNameEncoding, out var enabled) && enabled);
public static RequestCookieCollection Parse(StringValues values)
=> ParseInternal(values, AppContext.TryGetSwitch(ResponseCookies.EnableCookieNameEncoding, out var enabled) && enabled);

internal static RequestCookieCollection ParseInternal(IList<string> values, bool enableCookieNameEncoding)
internal static RequestCookieCollection ParseInternal(StringValues values, bool enableCookieNameEncoding)
{
if (values.Count == 0)
{
return Empty;
}
var collection = new RequestCookieCollection(values.Count);
var store = collection.Store!;

if (CookieHeaderValue.TryParseList(values, out var cookies))
if (CookieHeaderParserShared.TryParseValues(values, store, enableCookieNameEncoding, supportsMultipleValues: true))
{
if (cookies.Count == 0)
if (store.Count == 0)
{
return Empty;
}

var collection = new RequestCookieCollection(cookies.Count);
var store = collection.Store!;
for (var i = 0; i < cookies.Count; i++)
{
var cookie = cookies[i];
var name = enableCookieNameEncoding ? Uri.UnescapeDataString(cookie.Name.Value) : cookie.Name.Value;
var value = Uri.UnescapeDataString(cookie.Value.Value);
store[name] = value;
}

return collection;
}
return Empty;
Expand Down
5 changes: 4 additions & 1 deletion src/Http/Http/src/Microsoft.AspNetCore.Http.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>ASP.NET Core default HTTP feature implementations.</Description>
Expand All @@ -15,6 +15,9 @@
<Compile Include="$(SharedSourceRoot)CopyOnWriteDictionary\*.cs" />
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" />
<Compile Include="..\..\Shared\StreamCopyOperationInternal.cs" Link="Internal\StreamCopyOperationInternal.cs" />
<Compile Include="..\..\Shared\CookieHeaderParserShared.cs" Link="Internal\CookieHeaderParserShared.cs" />
<Compile Include="..\..\Shared\HttpRuleParser.cs" LinkBase="Internal" />
<Compile Include="..\..\Shared\HttpParseResult.cs" LinkBase="Internal" />
<Compile Include="..\..\WebUtilities\src\AspNetCoreTempDirectory.cs" LinkBase="Internal" />
</ItemGroup>

Expand Down
1 change: 1 addition & 0 deletions src/Http/Http/src/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Http.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Http.MicroBenchmarks, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
Loading

0 comments on commit ec0841d

Please sign in to comment.