Reduce allocations on Conneg hotpath

This commit is contained in:
Ryan Nowak 2015-11-05 19:02:59 -08:00
parent f050e09283
commit 308dd109a0
7 changed files with 83 additions and 63 deletions

View File

@ -49,7 +49,7 @@ namespace Microsoft.Net.Http.Headers
private ICollection<string> _privateHeaders; private ICollection<string> _privateHeaders;
private bool _mustRevalidate; private bool _mustRevalidate;
private bool _proxyRevalidate; private bool _proxyRevalidate;
private ICollection<NameValueHeaderValue> _extensions; private IList<NameValueHeaderValue> _extensions;
public CacheControlHeaderValue() public CacheControlHeaderValue()
{ {
@ -158,7 +158,7 @@ namespace Microsoft.Net.Http.Headers
set { _proxyRevalidate = value; } set { _proxyRevalidate = value; }
} }
public ICollection<NameValueHeaderValue> Extensions public IList<NameValueHeaderValue> Extensions
{ {
get get
{ {

View File

@ -24,7 +24,7 @@ namespace Microsoft.Net.Http.Headers
= new GenericHeaderParser<ContentDispositionHeaderValue>(false, GetDispositionTypeLength); = new GenericHeaderParser<ContentDispositionHeaderValue>(false, GetDispositionTypeLength);
// Use list instead of dictionary since we may have multiple parameters with the same name. // Use list instead of dictionary since we may have multiple parameters with the same name.
private ICollection<NameValueHeaderValue> _parameters; private ObjectCollection<NameValueHeaderValue> _parameters;
private string _dispositionType; private string _dispositionType;
private ContentDispositionHeaderValue() private ContentDispositionHeaderValue()
@ -48,7 +48,7 @@ namespace Microsoft.Net.Http.Headers
} }
} }
public ICollection<NameValueHeaderValue> Parameters public IList<NameValueHeaderValue> Parameters
{ {
get get
{ {

View File

@ -13,7 +13,7 @@ namespace Microsoft.Net.Http.Headers
private const string QualityName = "q"; private const string QualityName = "q";
internal const string BytesUnit = "bytes"; internal const string BytesUnit = "bytes";
internal static void SetQuality(ICollection<NameValueHeaderValue> parameters, double? value) internal static void SetQuality(IList<NameValueHeaderValue> parameters, double? value)
{ {
Contract.Requires(parameters != null); Contract.Requires(parameters != null);
@ -50,7 +50,7 @@ namespace Microsoft.Net.Http.Headers
} }
} }
internal static double? GetQuality(ICollection<NameValueHeaderValue> parameters) internal static double? GetQuality(IList<NameValueHeaderValue> parameters)
{ {
Contract.Requires(parameters != null); Contract.Requires(parameters != null);

View File

@ -59,7 +59,7 @@ namespace Microsoft.Net.Http.Headers
// Remove charset parameter // Remove charset parameter
if (charsetParameter != null) if (charsetParameter != null)
{ {
_parameters.Remove(charsetParameter); Parameters.Remove(charsetParameter);
} }
} }
else else
@ -123,7 +123,7 @@ namespace Microsoft.Net.Http.Headers
// Remove charset parameter // Remove charset parameter
if (boundaryParameter != null) if (boundaryParameter != null)
{ {
_parameters.Remove(boundaryParameter); Parameters.Remove(boundaryParameter);
} }
} }
else else
@ -140,7 +140,7 @@ namespace Microsoft.Net.Http.Headers
} }
} }
public ICollection<NameValueHeaderValue> Parameters public IList<NameValueHeaderValue> Parameters
{ {
get get
{ {
@ -161,8 +161,12 @@ namespace Microsoft.Net.Http.Headers
public double? Quality public double? Quality
{ {
get { return HeaderUtilities.GetQuality(Parameters); } get { return HeaderUtilities.GetQuality(_parameters); }
set { HeaderUtilities.SetQuality(Parameters, value); } set
{
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
HeaderUtilities.SetQuality(Parameters, value);
}
} }
public string MediaType public string MediaType
@ -210,7 +214,7 @@ namespace Microsoft.Net.Http.Headers
{ {
get get
{ {
return SubType.Equals("*", StringComparison.Ordinal); return string.Compare(_mediaType, _mediaType.IndexOf('/') + 1, "*", 0, 1, StringComparison.Ordinal) == 0;
} }
} }
@ -241,15 +245,31 @@ namespace Microsoft.Net.Http.Headers
return false; return false;
} }
// PERF: Avoid doing anything here that allocates a substring, this is a very hot path
// for content-negotiation.
var indexOfSlash = _mediaType.IndexOf('/');
// "text/plain" is a subset of "text/plain", "text/*" and "*/*". "*/*" is a subset only of "*/*". // "text/plain" is a subset of "text/plain", "text/*" and "*/*". "*/*" is a subset only of "*/*".
if (!Type.Equals(otherMediaType.Type, StringComparison.OrdinalIgnoreCase)) if (string.Compare(
strA: _mediaType,
indexA: 0,
strB: otherMediaType._mediaType,
indexB: 0,
length: indexOfSlash,
comparisonType: StringComparison.OrdinalIgnoreCase) != 0)
{ {
if (!otherMediaType.MatchesAllTypes) if (!otherMediaType.MatchesAllTypes)
{ {
return false; return false;
} }
} }
else if (!SubType.Equals(otherMediaType.SubType, StringComparison.OrdinalIgnoreCase)) else if (string.Compare(
strA: MediaType,
indexA: indexOfSlash + 1,
strB: otherMediaType._mediaType,
indexB: indexOfSlash + 1, // We know the Type is equal, so the index of '/' is the same in both strings.
length: _mediaType.Length - indexOfSlash,
comparisonType: StringComparison.OrdinalIgnoreCase) != 0)
{ {
if (!otherMediaType.MatchesAllSubTypes) if (!otherMediaType.MatchesAllSubTypes)
{ {
@ -457,17 +477,17 @@ namespace Microsoft.Net.Http.Headers
// If there is no whitespace between <type> and <subtype> in <type>/<subtype> get the media type using // If there is no whitespace between <type> and <subtype> in <type>/<subtype> get the media type using
// one Substring call. Otherwise get substrings for <type> and <subtype> and combine them. // one Substring call. Otherwise get substrings for <type> and <subtype> and combine them.
var mediatTypeLength = current + subtypeLength - startIndex; var mediaTypeLength = current + subtypeLength - startIndex;
if (typeLength + subtypeLength + 1 == mediatTypeLength) if (typeLength + subtypeLength + 1 == mediaTypeLength)
{ {
mediaType = input.Substring(startIndex, mediatTypeLength); mediaType = input.Substring(startIndex, mediaTypeLength);
} }
else else
{ {
mediaType = input.Substring(startIndex, typeLength) + "/" + input.Substring(current, subtypeLength); mediaType = input.Substring(startIndex, typeLength) + "/" + input.Substring(current, subtypeLength);
} }
return mediatTypeLength; return mediaTypeLength;
} }
private static void CheckMediaTypeFormat(string mediaType, string parameterName) private static void CheckMediaTypeFormat(string mediaType, string parameterName)

View File

@ -173,7 +173,7 @@ namespace Microsoft.Net.Http.Headers
} }
internal static void ToString( internal static void ToString(
ICollection<NameValueHeaderValue> values, IList<NameValueHeaderValue> values,
char separator, char separator,
bool leadingSeparator, bool leadingSeparator,
StringBuilder destination) StringBuilder destination)
@ -185,18 +185,18 @@ namespace Microsoft.Net.Http.Headers
return; return;
} }
foreach (var value in values) for (var i = 0; i < values.Count; i++)
{ {
if (leadingSeparator || (destination.Length > 0)) if (leadingSeparator || (destination.Length > 0))
{ {
destination.Append(separator); destination.Append(separator);
destination.Append(' '); destination.Append(' ');
} }
destination.Append(value.ToString()); destination.Append(values[i].ToString());
} }
} }
internal static string ToString(ICollection<NameValueHeaderValue> values, char separator, bool leadingSeparator) internal static string ToString(IList<NameValueHeaderValue> values, char separator, bool leadingSeparator)
{ {
if ((values == null) || (values.Count == 0)) if ((values == null) || (values.Count == 0))
{ {
@ -210,7 +210,7 @@ namespace Microsoft.Net.Http.Headers
return sb.ToString(); return sb.ToString();
} }
internal static int GetHashCode(ICollection<NameValueHeaderValue> values) internal static int GetHashCode(IList<NameValueHeaderValue> values)
{ {
if ((values == null) || (values.Count == 0)) if ((values == null) || (values.Count == 0))
{ {
@ -218,9 +218,9 @@ namespace Microsoft.Net.Http.Headers
} }
var result = 0; var result = 0;
foreach (var value in values) for (var i = 0; i < values.Count; i++)
{ {
result = result ^ value.GetHashCode(); result = result ^ values[i].GetHashCode();
} }
return result; return result;
} }
@ -282,7 +282,7 @@ namespace Microsoft.Net.Http.Headers
string input, string input,
int startIndex, int startIndex,
char delimiter, char delimiter,
ICollection<NameValueHeaderValue> nameValueCollection) IList<NameValueHeaderValue> nameValueCollection)
{ {
Contract.Requires(nameValueCollection != null); Contract.Requires(nameValueCollection != null);
Contract.Requires(startIndex >= 0); Contract.Requires(startIndex >= 0);
@ -320,7 +320,7 @@ namespace Microsoft.Net.Http.Headers
} }
} }
public static NameValueHeaderValue Find(ICollection<NameValueHeaderValue> values, string name) public static NameValueHeaderValue Find(IList<NameValueHeaderValue> values, string name)
{ {
Contract.Requires((name != null) && (name.Length > 0)); Contract.Requires((name != null) && (name.Length > 0));
@ -329,8 +329,9 @@ namespace Microsoft.Net.Http.Headers
return null; return null;
} }
foreach (var value in values) for (var i = 0; i < values.Count; i++)
{ {
var value = values[i];
if (string.Compare(value.Name, name, StringComparison.OrdinalIgnoreCase) == 0) if (string.Compare(value.Name, name, StringComparison.OrdinalIgnoreCase) == 0)
{ {
return value; return value;

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
@ -12,15 +11,28 @@ namespace Microsoft.Net.Http.Headers
// type to throw if 'null' gets added. Collection<T> internally uses List<T> which comes at some cost. In addition // type to throw if 'null' gets added. Collection<T> internally uses List<T> which comes at some cost. In addition
// Collection<T>.Add() calls List<T>.InsertItem() which is an O(n) operation (compared to O(1) for List<T>.Add()). // Collection<T>.Add() calls List<T>.InsertItem() which is an O(n) operation (compared to O(1) for List<T>.Add()).
// This type is only used for very small collections (1-2 items) to keep the impact of using Collection<T> small. // This type is only used for very small collections (1-2 items) to keep the impact of using Collection<T> small.
internal class ObjectCollection<T> : ICollection<T> where T : class internal class ObjectCollection<T> : Collection<T> where T : class
{ {
internal static readonly Action<T> DefaultValidator = CheckNotNull; internal static readonly Action<T> DefaultValidator = CheckNotNull;
internal static readonly ObjectCollection<T> EmptyReadOnlyCollection internal static readonly ObjectCollection<T> EmptyReadOnlyCollection
= new ObjectCollection<T>(DefaultValidator, isReadOnly: true); = new ObjectCollection<T>(DefaultValidator, isReadOnly: true);
private readonly Collection<T> _collection = new Collection<T>();
private readonly Action<T> _validator; private readonly Action<T> _validator;
private readonly bool _isReadOnly;
// We need to create a 'read-only' inner list for Collection<T> to do the right
// thing.
private static IList<T> CreateInnerList(bool isReadOnly, IEnumerable <T> other = null)
{
var list = other == null ? new List<T>() : new List<T>(other);
if (isReadOnly)
{
return new ReadOnlyCollection<T>(list);
}
else
{
return list;
}
}
public ObjectCollection() public ObjectCollection()
: this(DefaultValidator) : this(DefaultValidator)
@ -28,58 +40,45 @@ namespace Microsoft.Net.Http.Headers
} }
public ObjectCollection(Action<T> validator, bool isReadOnly = false) public ObjectCollection(Action<T> validator, bool isReadOnly = false)
: base(CreateInnerList(isReadOnly))
{ {
_validator = validator; _validator = validator;
_isReadOnly = isReadOnly;
} }
public ObjectCollection(IEnumerable<T> other, bool isReadOnly = false) public ObjectCollection(IEnumerable<T> other, bool isReadOnly = false)
: base(CreateInnerList(isReadOnly, other))
{ {
_validator = DefaultValidator; _validator = DefaultValidator;
foreach (T item in other) foreach (T item in Items)
{ {
Add(item); _validator(item);
} }
_isReadOnly = isReadOnly;
} }
public int Count public bool IsReadOnly => ((ICollection<T>)this).IsReadOnly;
protected override void ClearItems()
{ {
get { return _collection.Count; } base.ClearItems();
} }
public bool IsReadOnly protected override void InsertItem(int index, T item)
{ {
get { return _isReadOnly; }
}
public void Add(T item)
{
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
_validator(item); _validator(item);
_collection.Add(item); base.InsertItem(index, item);
} }
public bool Remove(T item) protected override void RemoveItem(int index)
{ {
HeaderUtilities.ThrowIfReadOnly(IsReadOnly); base.RemoveItem(index);
return _collection.Remove(item);
} }
public void Clear() protected override void SetItem(int index, T item)
{ {
HeaderUtilities.ThrowIfReadOnly(IsReadOnly); _validator(item);
_collection.Clear(); base.SetItem(index, item);
} }
public bool Contains(T item) => _collection.Contains(item);
public void CopyTo(T[] array, int arrayIndex) => _collection.CopyTo(array, arrayIndex);
public IEnumerator<T> GetEnumerator() => _collection.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _collection.GetEnumerator();
private static void CheckNotNull(T item) private static void CheckNotNull(T item)
{ {
// null values cannot be added to the collection. // null values cannot be added to the collection.

View File

@ -122,9 +122,9 @@ namespace Microsoft.Net.Http.Headers
Assert.False(mediaType0.Parameters.IsReadOnly); Assert.False(mediaType0.Parameters.IsReadOnly);
Assert.True(mediaType1.Parameters.IsReadOnly); Assert.True(mediaType1.Parameters.IsReadOnly);
Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count); Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count);
Assert.Throws<InvalidOperationException>(() => mediaType1.Parameters.Add(new NameValueHeaderValue("name"))); Assert.Throws<NotSupportedException>(() => mediaType1.Parameters.Add(new NameValueHeaderValue("name")));
Assert.Throws<InvalidOperationException>(() => mediaType1.Parameters.Remove(new NameValueHeaderValue("name"))); Assert.Throws<NotSupportedException>(() => mediaType1.Parameters.Remove(new NameValueHeaderValue("name")));
Assert.Throws<InvalidOperationException>(() => mediaType1.Parameters.Clear()); Assert.Throws<NotSupportedException>(() => mediaType1.Parameters.Clear());
var pair0 = mediaType0.Parameters.First(); var pair0 = mediaType0.Parameters.First();
var pair1 = mediaType1.Parameters.First(); var pair1 = mediaType1.Parameters.First();