Skip to content

Continue work for QueryCollection #32829

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 17 commits into from
Jun 17, 2021
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
97 changes: 97 additions & 0 deletions src/Http/Http/perf/Microbenchmarks/QueryCollectionBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// 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 BenchmarkDotNet.Configs;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.WebUtilities;
using static Microsoft.AspNetCore.Http.Features.QueryFeature;

namespace Microsoft.AspNetCore.Http
{
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
public class QueryCollectionBenchmarks
{
private string _queryString;
private string _singleValue;
private string _singleValueWithPlus;
private string _encoded;

[IterationSetup]
public void Setup()
{
_queryString = "?key1=value1&key2=value2&key3=value3&key4=&key5=";
_singleValue = "?key1=value1";
_singleValueWithPlus = "?key1=value1+value2+value3";
_encoded = "?key1=value%231";
}

[Benchmark(Description = "ParseNew")]
[BenchmarkCategory("QueryString")]
public void ParseNew()
{
_ = QueryFeature.ParseNullableQueryInternal(_queryString);
}

[Benchmark(Description = "ParseNew")]
[BenchmarkCategory("Single")]
public void ParseNewSingle()
{
_ = QueryFeature.ParseNullableQueryInternal(_singleValue);
}

[Benchmark(Description = "ParseNew")]
[BenchmarkCategory("SingleWithPlus")]
public void ParseNewSingleWithPlus()
{
_ = QueryFeature.ParseNullableQueryInternal(_singleValueWithPlus);
}

[Benchmark(Description = "ParseNew")]
[BenchmarkCategory("Encoded")]
public void ParseNewEncoded()
{
_ = QueryFeature.ParseNullableQueryInternal(_encoded);
}

[Benchmark(Description = "QueryHelpersParse")]
[BenchmarkCategory("QueryString")]
public void QueryHelpersParse()
{
_ = QueryHelpers.ParseNullableQuery(_queryString);
}

[Benchmark(Description = "QueryHelpersParse")]
[BenchmarkCategory("Single")]
public void QueryHelpersParseSingle()
{
_ = QueryHelpers.ParseNullableQuery(_singleValue);
}

[Benchmark(Description = "QueryHelpersParse")]
[BenchmarkCategory("SingleWithPlus")]
public void QueryHelpersParseSingleWithPlus()
{
_ = QueryHelpers.ParseNullableQuery(_singleValueWithPlus);
}

[Benchmark(Description = "QueryHelpersParse")]
[BenchmarkCategory("Encoded")]
public void QueryHelpersParseEncoded()
{
_ = QueryHelpers.ParseNullableQuery(_encoded);
}

[Benchmark]
[BenchmarkCategory("Constructor")]
public void Constructor()
{
var dict = new KvpAccumulator();
if (dict.HasValues)
{
return;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public void Setup()
[Benchmark]
public void Parse_TypicalCookie()
{
RequestCookieCollection.Parse(_cookie);
_ = RequestCookieCollection.Parse(_cookie);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ public void Ctor_Values_RouteValueDictionary_EmptyArray()
}

[Benchmark]
public void Ctor_Values_RouteValueDictionary_Array()
public RouteValueDictionary Ctor_Values_RouteValueDictionary_Array()
{
new RouteValueDictionary(_arrayValues);
return new RouteValueDictionary(_arrayValues);
}

[Benchmark]
Expand Down
246 changes: 230 additions & 16 deletions src/Http/Http/src/Features/QueryFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using Microsoft.AspNetCore.WebUtilities;
using System.Buffers;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using Microsoft.AspNetCore.Internal;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.Http.Features
{
Expand Down Expand Up @@ -55,30 +62,21 @@ public IQueryCollection Query
{
get
{
if (_features.Collection == null)
if (_features.Collection is null)
{
if (_parsedValues == null)
{
_parsedValues = QueryCollection.Empty;
}
return _parsedValues;
return _parsedValues ?? QueryCollection.Empty;
}

var current = HttpRequestFeature.QueryString;
if (_parsedValues == null || !string.Equals(_original, current, StringComparison.Ordinal))
{
_original = current;

var result = QueryHelpers.ParseNullableQuery(current);
var result = ParseNullableQueryInternal(current);

if (result == null)
{
_parsedValues = QueryCollection.Empty;
}
else
{
_parsedValues = new QueryCollection(result);
}
_parsedValues = result is not null
? new QueryCollectionInternal(result)
: QueryCollection.Empty;
}
return _parsedValues;
}
Expand All @@ -100,5 +98,221 @@ public IQueryCollection Query
}
}
}

/// <summary>
/// Parse a query string into its component key and value parts.
/// </summary>
/// <param name="queryString">The raw query string value, with or without the leading '?'.</param>
/// <returns>A collection of parsed keys and values, null if there are no entries.</returns>
[SkipLocalsInit]
internal static AdaptiveCapacityDictionary<string, StringValues>? ParseNullableQueryInternal(string? queryString)
{
if (string.IsNullOrEmpty(queryString) || (queryString.Length == 1 && queryString[0] == '?'))
{
return null;
}

var accumulator = new KvpAccumulator();
var query = queryString.AsSpan();

if (query[0] == '?')
{
query = query[1..];
}

while (!query.IsEmpty)
{
var delimiterIndex = query.IndexOf('&');

var querySegment = delimiterIndex >= 0
? query.Slice(0, delimiterIndex)
: query;

var equalIndex = querySegment.IndexOf('=');

if (equalIndex >= 0)
{
var name = SpanHelper.ReplacePlusWithSpace(querySegment.Slice(0, equalIndex));
var value = SpanHelper.ReplacePlusWithSpace(querySegment.Slice(equalIndex + 1));

accumulator.Append(
Uri.UnescapeDataString(name),
Uri.UnescapeDataString(value));
}
else
{
if (!querySegment.IsEmpty)
{
accumulator.Append(querySegment);
}
}

if (delimiterIndex < 0)
{
break;
}

query = query.Slice(delimiterIndex + 1);
}

return accumulator.HasValues
? accumulator.GetResults()
: null;
}

internal struct KvpAccumulator
{
/// <summary>
/// This API supports infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
private AdaptiveCapacityDictionary<string, StringValues> _accumulator;
private AdaptiveCapacityDictionary<string, List<string>> _expandingAccumulator;

public void Append(ReadOnlySpan<char> key, ReadOnlySpan<char> value = default)
=> Append(key.ToString(), value.IsEmpty ? string.Empty : value.ToString());

/// <summary>
/// This API supports infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public void Append(string key, string value)
{
if (_accumulator is null)
{
_accumulator = new AdaptiveCapacityDictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
}

if (!_accumulator.TryGetValue(key, out var values))
{
// First value for this key
_accumulator[key] = new StringValues(value);
}
else
{
AppendToExpandingAccumulator(key, value, values);
}

ValueCount++;
}

private void AppendToExpandingAccumulator(string key, string value, StringValues values)
{
// When there are some values for the same key, so switch to expanding accumulator, and
// add a zero count marker in the accumulator to indicate that switch.

if (values.Count != 0)
{
_accumulator[key] = default;

if (_expandingAccumulator is null)
{
_expandingAccumulator = new AdaptiveCapacityDictionary<string, List<string>>(capacity: 5, StringComparer.OrdinalIgnoreCase);
}

// Already 2 (1 existing + the new one) entries so use List's expansion mechanism for more
var list = new List<string>();

list.AddRange(values);
list.Add(value);

_expandingAccumulator[key] = list;
}
else
{
// The marker indicates we are in the expanding accumulator, so just append to the list.
_expandingAccumulator[key].Add(value);
}
}

/// <summary>
/// This API supports infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public bool HasValues => ValueCount > 0;

/// <summary>
/// This API supports infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public int KeyCount => _accumulator?.Count ?? 0;

/// <summary>
/// This API supports infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public int ValueCount { get; private set; }

/// <summary>
/// This API supports infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public AdaptiveCapacityDictionary<string, StringValues> GetResults()
{
if (_expandingAccumulator != null)
{
// Coalesce count 3+ multi-value entries into _accumulator dictionary
foreach (var entry in _expandingAccumulator)
{
_accumulator[entry.Key] = new StringValues(entry.Value.ToArray());
}
}

return _accumulator ?? new AdaptiveCapacityDictionary<string, StringValues>(0, StringComparer.OrdinalIgnoreCase);
}
}

private static class SpanHelper
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like it should be in .NET proper.

{
private static readonly SpanAction<char, IntPtr> s_replacePlusWithSpace = ReplacePlusWithSpaceCore;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe string ReplacePlusWithSpace(ReadOnlySpan<char> span)
{
fixed (char* ptr = &MemoryMarshal.GetReference(span))
{
return string.Create(span.Length, (IntPtr)ptr, s_replacePlusWithSpace);
}
}

private static unsafe void ReplacePlusWithSpaceCore(Span<char> buffer, IntPtr state)
{
fixed (char* ptr = &MemoryMarshal.GetReference(buffer))
{
var input = (ushort*)state.ToPointer();
var output = (ushort*)ptr;

var i = (nint)0;
var n = (nint)(uint)buffer.Length;

if (Sse41.IsSupported && n >= Vector128<ushort>.Count)
{
var vecPlus = Vector128.Create((ushort)'+');
var vecSpace = Vector128.Create((ushort)' ');

do
{
var vec = Sse2.LoadVector128(input + i);
var mask = Sse2.CompareEqual(vec, vecPlus);
var res = Sse41.BlendVariable(vec, vecSpace, mask);
Sse2.Store(output + i, res);
i += Vector128<ushort>.Count;
} while (i <= n - Vector128<ushort>.Count);
}

for (; i < n; ++i)
{
if (input[i] != '+')
{
output[i] = input[i];
}
else
{
output[i] = ' ';
}
}
}
}
}
}
}
Loading