Remove allocations and improve Header Quality Values parsing performance

This commit is contained in:
Yves57 2017-02-17 22:44:38 +01:00 committed by John Luo
parent 81bec95b6a
commit 7440d5d29c
4 changed files with 108 additions and 21 deletions

View File

@ -13,6 +13,7 @@ namespace Microsoft.Net.Http.Headers
public static class HeaderUtilities
{
private static readonly int _int64MaxStringLength = 19;
private static readonly int _qualityValueMaxCharCount = 10; // Little bit more permissive than RFC7231 5.3.1
private const string QualityName = "q";
internal const string BytesUnit = "bytes";
@ -62,9 +63,8 @@ namespace Microsoft.Net.Http.Headers
{
// Note that the RFC requires decimal '.' regardless of the culture. I.e. using ',' as decimal
// separator is considered invalid (even if the current culture would allow it).
double qualityValue;
if (double.TryParse(qualityParameter.Value, NumberStyles.AllowDecimalPoint,
NumberFormatInfo.InvariantInfo, out qualityValue))
if (TryParseQualityDouble(qualityParameter.Value, 0, out var qualityValue, out var length))
{
return qualityValue;
}
@ -480,6 +480,95 @@ namespace Microsoft.Net.Http.Headers
}
}
// Strict and fast RFC7231 5.3.1 Quality value parser (and without memory allocation)
// See https://tools.ietf.org/html/rfc7231#section-5.3.1
// Check is made to verify if the value is between 0 and 1 (and it returns False if the check fails).
internal static bool TryParseQualityDouble(string input, int startIndex, out double quality, out int length)
{
quality = 0;
length = 0;
var inputLength = input.Length;
var current = startIndex;
var limit = startIndex + _qualityValueMaxCharCount;
var intPart = 0;
var decPart = 0;
var decPow = 1;
if (current >= inputLength)
{
return false;
}
var ch = input[current];
if (ch >= '0' && ch <= '1') // Only values between 0 and 1 are accepted, according to RFC
{
intPart = ch - '0';
current++;
}
else
{
// The RFC doesn't allow decimal values starting with dot. I.e. value ".123" is invalid. It must be in the
// form "0.123".
return false;
}
if (current < inputLength)
{
ch = input[current];
if (ch >= '0' && ch <= '9')
{
// The RFC accepts only one digit before the dot
return false;
}
if (ch == '.')
{
current++;
while (current < inputLength)
{
ch = input[current];
if (ch >= '0' && ch <= '9')
{
if (current >= limit)
{
return false;
}
decPart = decPart * 10 + ch - '0';
decPow *= 10;
current++;
}
else
{
break;
}
}
}
}
if (decPart != 0)
{
quality = intPart + decPart / (double)decPow;
}
else
{
quality = intPart;
}
if (quality < 0 || quality > 1)
{
return false;
}
length = current - startIndex;
return true;
}
/// <summary>
/// Converts the non-negative 64-bit numeric value to its equivalent string representation.
/// </summary>

View File

@ -61,13 +61,14 @@ namespace Microsoft.Net.Http.Headers
// 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.
parsedValues = null;
var results = new List<T>();
List<T> results = null;
if (values == null)
{
return false;
}
foreach (var value in values)
for (var i = 0; i < values.Count; i++)
{
var value = values[i];
int index = 0;
while (!string.IsNullOrEmpty(value) && index < value.Length)
@ -78,6 +79,10 @@ namespace Microsoft.Net.Http.Headers
// The entry may not contain an actual value, like " , "
if (output != null)
{
if (results == null)
{
results = new List<T>(); // Allocate it only when used
}
results.Add(output);
}
}
@ -92,7 +97,7 @@ namespace Microsoft.Net.Http.Headers
}
}
}
if (results.Count > 0)
if (results != null)
{
parsedValues = results;
return true;

View File

@ -204,21 +204,7 @@ namespace Microsoft.Net.Http.Headers
return false;
}
int qualityLength = HttpRuleParser.GetNumberLength(input, current, true);
if (qualityLength == 0)
{
return false;
}
double quality;
if (!double.TryParse(input.Substring(current, qualityLength), NumberStyles.AllowDecimalPoint,
NumberFormatInfo.InvariantInfo, out quality))
{
return false;
}
if ((quality < 0) || (quality > 1))
if (!HeaderUtilities.TryParseQualityDouble(input, current, out var quality, out var qualityLength))
{
return false;
}

View File

@ -121,6 +121,8 @@ namespace Microsoft.Net.Http.Headers
CheckValidParse(" t", new StringWithQualityHeaderValue("t"));
CheckValidParse("t;q=0.", new StringWithQualityHeaderValue("t", 0));
CheckValidParse("t;q=1.", new StringWithQualityHeaderValue("t", 1));
CheckValidParse("t;q=1.000", new StringWithQualityHeaderValue("t", 1));
CheckValidParse("t;q=0.12345678", new StringWithQualityHeaderValue("t", 0.12345678));
CheckValidParse("t ; q = 0", new StringWithQualityHeaderValue("t", 0));
CheckValidParse("iso-8859-5", new StringWithQualityHeaderValue("iso-8859-5"));
CheckValidParse("unicode-1-1; q=0.8", new StringWithQualityHeaderValue("unicode-1-1", 0.8));
@ -154,6 +156,11 @@ namespace Microsoft.Net.Http.Headers
[InlineData("t;q=a")]
[InlineData("t;qa")]
[InlineData("t;q1")]
[InlineData("integer_part_too_long;q=01")]
[InlineData("integer_part_too_long;q=01.0")]
[InlineData("decimal_part_too_long;q=0.123456789")]
[InlineData("decimal_part_too_long;q=0.123456789 ")]
[InlineData("no_integer_part;q=.1")]
public void Parse_SetOfInvalidValueStrings_Throws(string input)
{
Assert.Throws<FormatException>(() => StringWithQualityHeaderValue.Parse(input));