[Perf] Fully cache model metadata

This change caches the actual model metadata instances. Some profiling
showed we didn't go far enough, we were allocating a lot of ModelMetadata
+ ModelPropertyCollection instances.
This commit is contained in:
Ryan Nowak 2015-03-31 15:16:52 -07:00
parent 08f0e1055b
commit acb657d951
6 changed files with 122 additions and 109 deletions

View File

@ -7,21 +7,19 @@ using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
{
/// <summary>
/// A cache of metadata objects for a <see cref="DefaultModelMetadata"/>.
/// Holds associated metadata objects for a <see cref="DefaultModelMetadata"/>.
/// </summary>
/// <remarks>
/// These instances are shared by all <see cref="DefaultModelMetadata"/> instances representing
/// the same <see cref="Type"/>, property, or parameter. Any modifications to the data must be
/// thread-safe for multiple readers and writers.
/// Any modifications to the data must be thread-safe for multiple readers and writers.
/// </remarks>
public class DefaultMetadataDetailsCache
public class DefaultMetadataDetails
{
/// <summary>
/// Creates a new <see cref="DefaultMetadataDetailsCache"/>.
/// Creates a new <see cref="DefaultMetadataDetails"/>.
/// </summary>
/// <param name="key">The <see cref="ModelMetadataIdentity"/>.</param>
/// <param name="attributes">The set of model attributes.</param>
public DefaultMetadataDetailsCache(ModelMetadataIdentity key, IReadOnlyList<object> attributes)
public DefaultMetadataDetails(ModelMetadataIdentity key, IReadOnlyList<object> attributes)
{
Key = key;
Attributes = attributes;
@ -33,20 +31,25 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
public IReadOnlyList<object> Attributes { get; }
/// <summary>
/// Gets or sets the <see cref="Metadata.BindingMetadata"/>
/// Gets or sets the <see cref="Metadata.BindingMetadata"/>.
/// </summary>
public BindingMetadata BindingMetadata { get; set; }
/// <summary>
/// Gets or sets the <see cref="Metadata.DisplayMetadata"/>
/// Gets or sets the <see cref="Metadata.DisplayMetadata"/>.
/// </summary>
public DisplayMetadata DisplayMetadata { get; set; }
/// <summary>
/// Gets or sets the <see cref="ModelMetadataIdentity"/>
/// Gets or sets the <see cref="ModelMetadataIdentity"/>.
/// </summary>
public ModelMetadataIdentity Key { get; }
/// <summary>
/// Gets or sets the <see cref="ModelMetadata"/> entries for the model properties.
/// </summary>
public ModelMetadata[] Properties { get; set; }
/// <summary>
/// Gets or sets a property accessor delegate to get the property value from a model object.
/// </summary>

View File

@ -16,7 +16,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
{
private readonly IModelMetadataProvider _provider;
private readonly ICompositeMetadataDetailsProvider _detailsProvider;
private readonly DefaultMetadataDetailsCache _cache;
private readonly DefaultMetadataDetails _details;
private ReadOnlyDictionary<object, object> _additionalValues;
private bool? _isReadOnly;
@ -29,16 +29,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
/// </summary>
/// <param name="provider">The <see cref="IModelMetadataProvider"/>.</param>
/// <param name="detailsProvider">The <see cref="ICompositeMetadataDetailsProvider"/>.</param>
/// <param name="cache">The <see cref="DefaultMetadataDetailsCache"/>.</param>
/// <param name="details">The <see cref="DefaultMetadataDetails"/>.</param>
public DefaultModelMetadata(
[NotNull] IModelMetadataProvider provider,
[NotNull] ICompositeMetadataDetailsProvider detailsProvider,
[NotNull] DefaultMetadataDetailsCache cache)
: base(cache.Key)
[NotNull] DefaultMetadataDetails details)
: base(details.Key)
{
_provider = provider;
_detailsProvider = detailsProvider;
_cache = cache;
_details = details;
}
/// <summary>
@ -48,7 +48,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
{
get
{
return _cache.Attributes;
return _details.Attributes;
}
}
@ -62,14 +62,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
{
get
{
if (_cache.BindingMetadata == null)
if (_details.BindingMetadata == null)
{
var context = new BindingMetadataProviderContext(Identity, _cache.Attributes);
var context = new BindingMetadataProviderContext(Identity, _details.Attributes);
_detailsProvider.GetBindingMetadata(context);
_cache.BindingMetadata = context.BindingMetadata;
_details.BindingMetadata = context.BindingMetadata;
}
return _cache.BindingMetadata;
return _details.BindingMetadata;
}
}
@ -83,14 +83,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
{
get
{
if (_cache.DisplayMetadata == null)
if (_details.DisplayMetadata == null)
{
var context = new DisplayMetadataProviderContext(Identity, _cache.Attributes);
var context = new DisplayMetadataProviderContext(Identity, _details.Attributes);
_detailsProvider.GetDisplayMetadata(context);
_cache.DisplayMetadata = context.DisplayMetadata;
_details.DisplayMetadata = context.DisplayMetadata;
}
return _cache.DisplayMetadata;
return _details.DisplayMetadata;
}
}
@ -104,14 +104,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
{
get
{
if (_cache.ValidationMetadata == null)
if (_details.ValidationMetadata == null)
{
var context = new ValidationMetadataProviderContext(Identity, _cache.Attributes);
var context = new ValidationMetadataProviderContext(Identity, _details.Attributes);
_detailsProvider.GetValidationMetadata(context);
_cache.ValidationMetadata = context.ValidationMetadata;
_details.ValidationMetadata = context.ValidationMetadata;
}
return _cache.ValidationMetadata;
return _details.ValidationMetadata;
}
}
@ -286,7 +286,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
}
else
{
_isReadOnly = _cache.PropertySetter != null;
_isReadOnly = _details.PropertySetter != null;
}
}

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
public class DefaultModelMetadataProvider : IModelMetadataProvider
{
private readonly TypeCache _typeCache = new TypeCache();
private readonly PropertiesCache _propertiesCache = new PropertiesCache();
private readonly Func<ModelMetadataIdentity, ModelMetadataCacheEntry> _cacheEntryFactory;
/// <summary>
/// Creates a new <see cref="DefaultModelMetadataProvider"/>.
@ -24,6 +24,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
public DefaultModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider)
{
DetailsProvider = detailsProvider;
_cacheEntryFactory = CreateCacheEntry;
}
/// <summary>
@ -36,15 +38,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
{
var key = ModelMetadataIdentity.ForType(modelType);
var propertyEntries = _propertiesCache.GetOrAdd(key, CreatePropertyCacheEntries);
var cacheEntry = _typeCache.GetOrAdd(key, _cacheEntryFactory);
var properties = new ModelMetadata[propertyEntries.Length];
for (var i = 0; i < properties.Length; i++)
// We're relying on a safe race-condition for Properties - take care only
// to set the value onces the properties are fully-initialized.
if (cacheEntry.Details.Properties == null)
{
properties[i] = CreateModelMetadata(propertyEntries[i]);
var propertyDetails = CreatePropertyDetails(key);
var properties = new ModelMetadata[propertyDetails.Length];
for (var i = 0; i < properties.Length; i++)
{
properties[i] = CreateModelMetadata(propertyDetails[i]);
}
cacheEntry.Details.Properties = properties;
}
return properties;
return cacheEntry.Details.Properties;
}
/// <inheritdoc />
@ -52,85 +63,63 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
{
var key = ModelMetadataIdentity.ForType(modelType);
var entry = _typeCache.GetOrAdd(key, CreateTypeCacheEntry);
return CreateModelMetadata(entry);
var cacheEntry = _typeCache.GetOrAdd(key, _cacheEntryFactory);
return cacheEntry.Metadata;
}
private ModelMetadataCacheEntry CreateCacheEntry(ModelMetadataIdentity key)
{
var details = CreateTypeDetails(key);
var metadata = CreateModelMetadata(details);
return new ModelMetadataCacheEntry(metadata, details);
}
/// <summary>
/// Creates a new <see cref="ModelMetadata"/> from a <see cref="DefaultMetadataDetailsCache"/>.
/// Creates a new <see cref="ModelMetadata"/> from a <see cref="DefaultMetadataDetails"/>.
/// </summary>
/// <param name="entry">The <see cref="DefaultMetadataDetailsCache"/> entry with cached data.</param>
/// <param name="entry">The <see cref="DefaultMetadataDetails"/> entry with cached data.</param>
/// <returns>A new <see cref="ModelMetadata"/> instance.</returns>
/// <remarks>
/// <see cref="DefaultModelMetadataProvider"/> will always create instances of
/// <see cref="DefaultModelMetadata"/> .Override this method to create a <see cref="ModelMetadata"/>
/// of a different concrete type.
/// </remarks>
protected virtual ModelMetadata CreateModelMetadata(DefaultMetadataDetailsCache entry)
protected virtual ModelMetadata CreateModelMetadata(DefaultMetadataDetails entry)
{
return new DefaultModelMetadata(this, DetailsProvider, entry);
}
/// <summary>
/// Creates the <see cref="DefaultMetadataDetailsCache"/> entries for the properties of a model
/// Creates the <see cref="DefaultMetadataDetails"/> entries for the properties of a model
/// <see cref="Type"/>.
/// </summary>
/// <param name="key">
/// The <see cref="ModelMetadataIdentity"/> identifying the model <see cref="Type"/>.
/// </param>
/// <returns>A cache object for each property of the model <see cref="Type"/>.</returns>
/// <returns>A details object for each property of the model <see cref="Type"/>.</returns>
/// <remarks>
/// The results of this method will be cached and used to satisfy calls to
/// <see cref="GetMetadataForProperties(Type)"/>. Override this method to provide a different
/// set of property data.
/// </remarks>
protected virtual DefaultMetadataDetailsCache[] CreatePropertyCacheEntries([NotNull] ModelMetadataIdentity key)
protected virtual DefaultMetadataDetails[] CreatePropertyDetails([NotNull] ModelMetadataIdentity key)
{
var propertyHelpers = PropertyHelper.GetProperties(key.ModelType);
var propertyHelpers = PropertyHelper.GetVisibleProperties(key.ModelType);
var propertyEntries = new List<DefaultMetadataDetailsCache>(propertyHelpers.Length);
var propertyEntries = new List<DefaultMetadataDetails>(propertyHelpers.Length);
for (var i = 0; i < propertyHelpers.Length; i++)
{
var propertyHelper = propertyHelpers[i];
if (propertyHelper.Property.DeclaringType != key.ModelType)
{
// If this property was declared on a base type then look for the definition closest to the
// the model type to see if we should include it.
var ignoreProperty = false;
// Walk up the hierarchy until we find the type that actally declares this
// PropertyInfo.
var currentType = key.ModelType.GetTypeInfo();
while (currentType != propertyHelper.Property.DeclaringType.GetTypeInfo())
{
// We've found a 'more proximal' public definition
var declaredProperty = currentType.GetDeclaredProperty(propertyHelper.Name);
if (declaredProperty != null)
{
ignoreProperty = true;
break;
}
currentType = currentType.BaseType.GetTypeInfo();
}
if (ignoreProperty)
{
// There's a better definition, ignore this.
continue;
}
}
var propertyKey = ModelMetadataIdentity.ForProperty(
propertyHelper.Property.PropertyType,
propertyHelper.Name,
key.ModelType);
var attributes = new List<object>(ModelAttributes.GetAttributesForProperty(
key.ModelType,
key.ModelType,
propertyHelper.Property));
var propertyEntry = new DefaultMetadataDetailsCache(propertyKey, attributes);
var propertyEntry = new DefaultMetadataDetails(propertyKey, attributes);
if (propertyHelper.Property.CanRead && propertyHelper.Property.GetMethod?.IsPrivate == true)
{
propertyEntry.PropertyAccessor = PropertyHelper.MakeFastPropertyGetter(propertyHelper.Property);
@ -148,24 +137,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
}
/// <summary>
/// Creates the <see cref="DefaultMetadataDetailsCache"/> entry for a model <see cref="Type"/>.
/// Creates the <see cref="DefaultMetadataDetails"/> entry for a model <see cref="Type"/>.
/// </summary>
/// <param name="key">
/// The <see cref="ModelMetadataIdentity"/> identifying the model <see cref="Type"/>.
/// </param>
/// <returns>A cache object for the model <see cref="Type"/>.</returns>
/// <returns>A details object for the model <see cref="Type"/>.</returns>
/// <remarks>
/// The results of this method will be cached and used to satisfy calls to
/// <see cref="GetMetadataForType(Type)"/>. Override this method to provide a different
/// set of attributes.
/// </remarks>
protected virtual DefaultMetadataDetailsCache CreateTypeCacheEntry([NotNull] ModelMetadataIdentity key)
protected virtual DefaultMetadataDetails CreateTypeDetails([NotNull] ModelMetadataIdentity key)
{
var attributes = new List<object>(ModelAttributes.GetAttributesForType(key.ModelType));
return new DefaultMetadataDetailsCache(key, attributes);
return new DefaultMetadataDetails(key, attributes);
}
private class TypeCache : ConcurrentDictionary<ModelMetadataIdentity, DefaultMetadataDetailsCache>
private class TypeCache : ConcurrentDictionary<ModelMetadataIdentity, ModelMetadataCacheEntry>
{
public TypeCache()
: base(ModelMetadataIdentityComparer.Instance)
@ -173,12 +162,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
}
}
private class PropertiesCache : ConcurrentDictionary<ModelMetadataIdentity, DefaultMetadataDetailsCache[]>
private struct ModelMetadataCacheEntry
{
public PropertiesCache()
: base(ModelMetadataIdentityComparer.Instance)
public ModelMetadataCacheEntry(ModelMetadata metadata, DefaultMetadataDetails details)
{
Metadata = metadata;
Details = details;
}
public ModelMetadata Metadata { get; private set; }
public DefaultMetadataDetails Details { get; private set; }
}
private class ModelMetadataIdentityComparer : IEqualityComparer<ModelMetadataIdentity>

View File

@ -26,7 +26,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
Assert.Equal("OnType", attribute.Value);
}
// The attributes and other 'details' are cached
[Fact]
public void GetMetadataForType_Cached()
{
@ -38,6 +37,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
var metadata2 = Assert.IsType<DefaultModelMetadata>(provider.GetMetadataForType(typeof(ModelType)));
// Assert
Assert.Same(metadata1, metadata2);
Assert.Same(metadata1.Attributes, metadata2.Attributes);
Assert.Same(metadata1.BindingMetadata, metadata2.BindingMetadata);
Assert.Same(metadata1.DisplayMetadata, metadata2.DisplayMetadata);
@ -80,19 +80,35 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
var provider = CreateProvider();
// Act
var metadata1 = provider.GetMetadataForProperties(typeof(ModelType)).Cast<DefaultModelMetadata>().ToArray();
var metadata2 = provider.GetMetadataForProperties(typeof(ModelType)).Cast<DefaultModelMetadata>().ToArray();
var properties1 = provider.GetMetadataForProperties(typeof(ModelType)).Cast<DefaultModelMetadata>().ToArray();
var properties2 = provider.GetMetadataForProperties(typeof(ModelType)).Cast<DefaultModelMetadata>().ToArray();
// Assert
for (var i = 0; i < metadata1.Length; i++)
Assert.Equal(properties1.Length, properties2.Length);
for (var i = 0; i < properties1.Length; i++)
{
Assert.Same(metadata1[i].Attributes, metadata2[i].Attributes);
Assert.Same(metadata1[i].BindingMetadata, metadata2[i].BindingMetadata);
Assert.Same(metadata1[i].DisplayMetadata, metadata2[i].DisplayMetadata);
Assert.Same(metadata1[i].ValidationMetadata, metadata2[i].ValidationMetadata);
Assert.Same(properties1[i], properties2[i]);
Assert.Same(properties1[i].Attributes, properties2[i].Attributes);
Assert.Same(properties1[i].BindingMetadata, properties2[i].BindingMetadata);
Assert.Same(properties1[i].DisplayMetadata, properties2[i].DisplayMetadata);
Assert.Same(properties1[i].ValidationMetadata, properties2[i].ValidationMetadata);
}
}
[Fact]
public void GetMetadataForType_PropertiesCollection_Cached()
{
// Arrange
var provider = CreateProvider();
// Act
var metadata1 = Assert.IsType<DefaultModelMetadata>(provider.GetMetadataForType(typeof(ModelType)));
var metadata2 = Assert.IsType<DefaultModelMetadata>(provider.GetMetadataForType(typeof(ModelType)));
// Assert
Assert.Same(metadata1.Properties, metadata2.Properties);
}
[Fact]
public void GetMetadataForProperties_IncludesMergedAttributes()
{

View File

@ -23,7 +23,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
Enumerable.Empty<IMetadataDetailsProvider>());
var key = ModelMetadataIdentity.ForType(typeof(string));
var cache = new DefaultMetadataDetailsCache(key, new object[0]);
var cache = new DefaultMetadataDetails(key, new object[0]);
// Act
var metadata = new DefaultModelMetadata(provider, detailsProvider, cache);
@ -76,7 +76,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
Enumerable.Empty<IMetadataDetailsProvider>());
var key = ModelMetadataIdentity.ForType(typeof(Exception));
var cache = new DefaultMetadataDetailsCache(key, new object[0]);
var cache = new DefaultMetadataDetails(key, new object[0]);
// Act
var metadata = new DefaultModelMetadata(provider, detailsProvider, cache);
@ -95,7 +95,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
var detailsProvider = new EmptyCompositeMetadataDetailsProvider();
var key = ModelMetadataIdentity.ForProperty(typeof(string), "Message", typeof(Exception));
var cache = new DefaultMetadataDetailsCache(key, new object[0]);
var cache = new DefaultMetadataDetails(key, new object[0]);
// Act
var metadata = new DefaultModelMetadata(provider, detailsProvider, cache);
@ -117,7 +117,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
var detailsProvider = new EmptyCompositeMetadataDetailsProvider();
var key = ModelMetadataIdentity.ForType(modelType);
var cache = new DefaultMetadataDetailsCache(key, new object[0]);
var cache = new DefaultMetadataDetails(key, new object[0]);
var metadata = new DefaultModelMetadata(provider, detailsProvider, cache);
@ -138,7 +138,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
var detailsProvider = new EmptyCompositeMetadataDetailsProvider();
var key = ModelMetadataIdentity.ForType(modelType);
var cache = new DefaultMetadataDetailsCache(key, new object[0]);
var cache = new DefaultMetadataDetails(key, new object[0]);
var metadata = new DefaultModelMetadata(provider, detailsProvider, cache);
@ -162,13 +162,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
new DefaultModelMetadata(
provider.Object,
detailsProvider,
new DefaultMetadataDetailsCache(
new DefaultMetadataDetails(
ModelMetadataIdentity.ForProperty(typeof(int), "Prop1", typeof(string)),
attributes: null)),
new DefaultModelMetadata(
provider.Object,
detailsProvider,
new DefaultMetadataDetailsCache(
new DefaultMetadataDetails(
ModelMetadataIdentity.ForProperty(typeof(int), "Prop2", typeof(string)),
attributes: null)),
};
@ -178,7 +178,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
.Returns(expectedProperties);
var key = ModelMetadataIdentity.ForType(typeof(string));
var cache = new DefaultMetadataDetailsCache(key, new object[0]);
var cache = new DefaultMetadataDetails(key, new object[0]);
var metadata = new DefaultModelMetadata(provider.Object, detailsProvider, cache);
@ -238,7 +238,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
expectedProperties.Add(new DefaultModelMetadata(
provider.Object,
detailsProvider,
new DefaultMetadataDetailsCache(
new DefaultMetadataDetails(
ModelMetadataIdentity.ForProperty(typeof(int), originalName, typeof(string)),
attributes: null)));
}
@ -248,7 +248,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
.Returns(expectedProperties);
var key = ModelMetadataIdentity.ForType(typeof(string));
var cache = new DefaultMetadataDetailsCache(key, new object[0]);
var cache = new DefaultMetadataDetails(key, new object[0]);
var metadata = new DefaultModelMetadata(provider.Object, detailsProvider, cache);
@ -338,7 +338,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
var expectedProperties = new List<DefaultModelMetadata>();
foreach (var kvp in originalNamesAndOrders)
{
var propertyCache = new DefaultMetadataDetailsCache(
var propertyCache = new DefaultMetadataDetails(
ModelMetadataIdentity.ForProperty(typeof(int), kvp.Key, typeof(string)),
attributes: null);
@ -356,7 +356,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
.Returns(expectedProperties);
var key = ModelMetadataIdentity.ForType(typeof(string));
var cache = new DefaultMetadataDetailsCache(key, new object[0]);
var cache = new DefaultMetadataDetails(key, new object[0]);
var metadata = new DefaultModelMetadata(provider.Object, detailsProvider, cache);
@ -377,7 +377,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
var detailsProvider = new EmptyCompositeMetadataDetailsProvider();
var key = ModelMetadataIdentity.ForType(typeof(string));
var cache = new DefaultMetadataDetailsCache(key, new object[0]);
var cache = new DefaultMetadataDetails(key, new object[0]);
var metadata = new DefaultModelMetadata(provider, detailsProvider, cache);
@ -398,7 +398,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
var detailsProvider = new EmptyCompositeMetadataDetailsProvider();
var key = ModelMetadataIdentity.ForType(typeof(string));
var cache = new DefaultMetadataDetailsCache(key, new object[0]);
var cache = new DefaultMetadataDetails(key, new object[0]);
var metadata = new DefaultModelMetadata(provider, detailsProvider, cache);

View File

@ -838,10 +838,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata
_attributes = attributes;
}
protected override DefaultMetadataDetailsCache CreateTypeCacheEntry(ModelMetadataIdentity key)
protected override DefaultMetadataDetails CreateTypeDetails([NotNull]ModelMetadataIdentity key)
{
var entry = base.CreateTypeCacheEntry(key);
return new DefaultMetadataDetailsCache(key, _attributes.Concat(entry.Attributes).ToArray());
var entry = base.CreateTypeDetails(key);
return new DefaultMetadataDetails(key, _attributes.Concat(entry.Attributes).ToArray());
}
}
}