-
Notifications
You must be signed in to change notification settings - Fork 10.3k
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
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
793b523
Working on dictionary for query
jkotalik ec818d0
Make initial capacity smaller when not specifying capacity
jkotalik 18d4aff
Change defaults for dictionary
jkotalik 11c3afa
Updating some benchmarks
jkotalik 8012a72
remove comment
jkotalik 78d93fa
Merge branch 'main' into dictMorePlaces
gfoidl 4468559
Updated benchmarks
gfoidl ed2afaf
Span-based version for ParseNullableQueryInternal
gfoidl 05ebdcf
Cleaned span based version
gfoidl b06ca3a
Fixed test failure
gfoidl 3685ee2
Added benchmark case for % encoded values
gfoidl a6353d1
Port over tests from QueryHelpersTests + fixes
gfoidl b9f61a0
Combine ReplacePlusWithSpace and string materialization and void span…
gfoidl 41a4dd7
PR feedback
gfoidl bbcda61
Restructured KvpAccumulator.Append
gfoidl 719eff4
Update src/Http/Http/src/Features/QueryFeature.cs
gfoidl a050fae
Revert import of $(SharedSourceRoot)UrlDecoder
gfoidl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
97 changes: 97 additions & 0 deletions
97
src/Http/Http/perf/Microbenchmarks/QueryCollectionBenchmarks.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
{ | ||
|
@@ -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; | ||
} | ||
|
@@ -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) | ||
Tratcher marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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] = ' '; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.