Skip to content
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

Reduce allocations for Cookies. #31258

Merged
merged 6 commits into from
Mar 26, 2021
Merged
Show file tree
Hide file tree
Changes from 4 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
68 changes: 65 additions & 3 deletions src/Http/Headers/src/CookieHeaderParser.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// 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;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using Microsoft.Extensions.Primitives;

Expand All @@ -13,8 +16,9 @@ internal CookieHeaderParser(bool supportsMultipleValues)
{
}

public override bool TryParseValue(StringSegment value, ref int index, out CookieHeaderValue? parsedValue)
public bool TryParseValue(StringSegment value, ref int index, [NotNullWhen(true)] out StringSegment? parsedName, [NotNullWhen(true)] out StringSegment? parsedValue)
{
parsedName = null;
parsedValue = null;

// If multiple values are supported (i.e. list of values), then accept an empty string: The header may
Expand Down Expand Up @@ -43,7 +47,7 @@ public override bool TryParseValue(StringSegment value, ref int index, out Cooki
return SupportsMultipleValues;
}

if (!CookieHeaderValue.TryGetCookieLength(value, ref current, out var result))
if (!CookieHeaderValue.TryGetCookieLength(value, ref current, out parsedName, out parsedValue))
{
return false;
}
Expand All @@ -57,7 +61,65 @@ public override bool TryParseValue(StringSegment value, ref int index, out Cooki
}

index = current;
parsedValue = result;

return true;
}

public bool TryParseValues(StringValues values, Dictionary<string, string> store, bool enableCookieNameEncoding)
{
// If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller
// can ignore the value.
if (values.Count == 0)
{
return false;
}
bool hasFoundValue = false;

for (var i = 0; i < values.Count; i++)
{
var value = values[i];
var index = 0;

while (!string.IsNullOrEmpty(value) && index < value.Length)
{
if (TryParseValue(value, ref index, out var parsedName, out var parsedValue))
{
// The entry may not contain an actual value, like " , "
if (parsedName != null && parsedValue != null)
{
var name = enableCookieNameEncoding ? Uri.UnescapeDataString(parsedName.Value.Value) : parsedName.Value.Value;
jkotalik marked this conversation as resolved.
Show resolved Hide resolved
store[name] = Uri.UnescapeDataString(parsedValue.Value.Value);
hasFoundValue = true;
}
}
else
{
// Skip the invalid values and keep trying.
index++;
}
}
}

return hasFoundValue;
}

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

if (!TryParseValue(value, ref index, out var parsedName, out var parsedValue))
{
return false;
}

if (parsedName == null || parsedValue == null)
{
// Successfully parsed, but no values.
return true;
}

cookieValue = new CookieHeaderValue(parsedName.Value, parsedValue.Value);

return true;
}

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

/// <summary>
jkotalik marked this conversation as resolved.
Show resolved Hide resolved
/// Attempts to parse the sequence of values into a dictionary of cookies using string parsing rules.
/// </summary>
/// <param name="inputs">The values to parse.</param>
/// <param name="store"></param>
/// <param name="enableCookieNameEncoding"></param>
/// <returns></returns>
public static bool TryParseIntoDictionary(StringValues inputs, Dictionary<string, string> store, bool enableCookieNameEncoding)
{
return MultipleValueParser.TryParseValues(inputs, store, enableCookieNameEncoding);
}

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

parsedName = null;
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;
Expand All @@ -189,7 +200,8 @@ internal static bool TryGetCookieLength(StringSegment input, ref int offset, [No
{
return false;
}
result._name = input.Subsegment(offset, itemLength);

parsedName = input.Subsegment(offset, itemLength);
offset += itemLength;

// = (no spaces)
Expand All @@ -200,9 +212,8 @@ internal static bool TryGetCookieLength(StringSegment input, ref int offset, [No

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

parsedValue = result;
return true;
}

Expand Down
1 change: 1 addition & 0 deletions src/Http/Headers/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
*REMOVED*Microsoft.Net.Http.Headers.RangeConditionHeaderValue.RangeConditionHeaderValue(Microsoft.Net.Http.Headers.EntityTagHeaderValue? entityTag) -> void
Microsoft.Net.Http.Headers.MediaTypeHeaderValue.MatchesMediaType(Microsoft.Extensions.Primitives.StringSegment otherMediaType) -> bool
Microsoft.Net.Http.Headers.RangeConditionHeaderValue.RangeConditionHeaderValue(Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag) -> void
static Microsoft.Net.Http.Headers.CookieHeaderValue.TryParseIntoDictionary(Microsoft.Extensions.Primitives.StringValues inputs, System.Collections.Generic.Dictionary<string!, string!>! store, bool enableCookieNameEncoding) -> bool
jkotalik marked this conversation as resolved.
Show resolved Hide resolved
static readonly Microsoft.Net.Http.Headers.HeaderNames.Baggage -> string!
static readonly Microsoft.Net.Http.Headers.HeaderNames.ProxyConnection -> string!
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 (CookieHeaderValue.TryParseIntoDictionary(values, store, enableCookieNameEncoding))
{
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
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")]