Replace HashSet with a Hybrid HashSet/List
to improve performance on less nested models
This commit is contained in:
parent
3fdcaecaa8
commit
60c59b576e
|
|
@ -0,0 +1,73 @@
|
||||||
|
// 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 System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Internal
|
||||||
|
{
|
||||||
|
public class ValidationStack
|
||||||
|
{
|
||||||
|
public int Count => HashSet?.Count ?? List.Count;
|
||||||
|
|
||||||
|
// We tested the performance of a list at size 15 and found it still better than hashset, but to avoid a costly
|
||||||
|
// O(n) search at larger n we set the cutoff to 20. If someone finds the point where they intersect feel free to change this number.
|
||||||
|
internal const int CutOff = 20;
|
||||||
|
|
||||||
|
internal List<object> List { get; } = new List<object>();
|
||||||
|
|
||||||
|
internal HashSet<object> HashSet { get; set; }
|
||||||
|
|
||||||
|
public bool Push(object model)
|
||||||
|
{
|
||||||
|
if (HashSet != null)
|
||||||
|
{
|
||||||
|
return HashSet.Add(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ListContains(model))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List.Add(model);
|
||||||
|
|
||||||
|
if (HashSet == null && List.Count > CutOff)
|
||||||
|
{
|
||||||
|
HashSet = new HashSet<object>(List, ReferenceEqualityComparer.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Pop(object model)
|
||||||
|
{
|
||||||
|
if (HashSet != null)
|
||||||
|
{
|
||||||
|
HashSet.Remove(model);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (model != null)
|
||||||
|
{
|
||||||
|
Debug.Assert(ReferenceEquals(List[List.Count - 1], model));
|
||||||
|
List.RemoveAt(List.Count - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ListContains(object model)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < List.Count; i++)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(model, List[i]))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using Microsoft.AspNetCore.Mvc.Internal;
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
|
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
|
||||||
{
|
{
|
||||||
|
|
@ -27,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
|
||||||
private ModelMetadata _metadata;
|
private ModelMetadata _metadata;
|
||||||
private IValidationStrategy _strategy;
|
private IValidationStrategy _strategy;
|
||||||
|
|
||||||
private HashSet<object> _currentPath;
|
private ValidationStack _currentPath;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="ValidationVisitor"/>.
|
/// Creates a new <see cref="ValidationVisitor"/>.
|
||||||
|
|
@ -67,7 +68,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
|
||||||
_validationState = validationState;
|
_validationState = validationState;
|
||||||
|
|
||||||
_modelState = actionContext.ModelState;
|
_modelState = actionContext.ModelState;
|
||||||
_currentPath = new HashSet<object>(ReferenceEqualityComparer.Instance);
|
_currentPath = new ValidationStack();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -156,7 +157,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
|
||||||
{
|
{
|
||||||
RuntimeHelpers.EnsureSufficientExecutionStack();
|
RuntimeHelpers.EnsureSufficientExecutionStack();
|
||||||
|
|
||||||
if (model != null && !_currentPath.Add(model))
|
if (model != null && !_currentPath.Push(model))
|
||||||
{
|
{
|
||||||
// This is a cycle, bail.
|
// This is a cycle, bail.
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -176,6 +177,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
|
||||||
{
|
{
|
||||||
// Use the key on the entry, because we might not have entries in model state.
|
// Use the key on the entry, because we might not have entries in model state.
|
||||||
SuppressValidation(entry.Key);
|
SuppressValidation(entry.Key);
|
||||||
|
_currentPath.Pop(model);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -352,7 +354,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
|
||||||
_visitor._model = _model;
|
_visitor._model = _model;
|
||||||
_visitor._strategy = _strategy;
|
_visitor._strategy = _strategy;
|
||||||
|
|
||||||
_visitor._currentPath.Remove(_newModel);
|
_visitor._currentPath.Pop(_newModel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -557,6 +557,42 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Assert.Empty(entry.Errors);
|
Assert.Empty(entry.Errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[ReplaceCulture]
|
||||||
|
public void Validate_ComplexType_SecondLevelCyclesNotFollowed_Invalid()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actionContext = new ActionContext();
|
||||||
|
var modelState = actionContext.ModelState;
|
||||||
|
var validationState = new ValidationStateDictionary();
|
||||||
|
|
||||||
|
var validator = CreateValidator();
|
||||||
|
|
||||||
|
var person = new Person() { Name = "Billy" };
|
||||||
|
person.Family = new Family { Members = new List<Person> { person } };
|
||||||
|
|
||||||
|
var model = (object)person;
|
||||||
|
|
||||||
|
modelState.SetModelValue("parameter.Name", "Billy", "Billy");
|
||||||
|
validationState.Add(model, new ValidationStateEntry() { Key = "parameter" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
validator.Validate(actionContext, validationState, "parameter", model);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(modelState.IsValid);
|
||||||
|
AssertKeysEqual(modelState, "parameter.Name", "parameter.Profession");
|
||||||
|
|
||||||
|
var entry = modelState["parameter.Name"];
|
||||||
|
Assert.Equal(ModelValidationState.Valid, entry.ValidationState);
|
||||||
|
Assert.Empty(entry.Errors);
|
||||||
|
|
||||||
|
entry = modelState["parameter.Profession"];
|
||||||
|
Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
|
||||||
|
var error = Assert.Single(entry.Errors);
|
||||||
|
Assert.Equal(error.ErrorMessage, ValidationAttributeUtil.GetRequiredErrorMessage("Profession"));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[ReplaceCulture]
|
[ReplaceCulture]
|
||||||
public void Validate_ComplexType_CyclesNotFollowed_Invalid()
|
public void Validate_ComplexType_CyclesNotFollowed_Invalid()
|
||||||
|
|
@ -1138,6 +1174,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
public string Profession { get; set; }
|
public string Profession { get; set; }
|
||||||
|
|
||||||
public Person Friend { get; set; }
|
public Person Friend { get; set; }
|
||||||
|
|
||||||
|
public Family Family { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Family
|
||||||
|
{
|
||||||
|
public List<Person> Members { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Person2
|
private class Person2
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
// 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 System.Collections.Generic;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Internal
|
||||||
|
{
|
||||||
|
public class ValidationStackTest
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0)]
|
||||||
|
[InlineData(5)]
|
||||||
|
[InlineData(ValidationStack.CutOff + 1)]
|
||||||
|
public void Push_ReturnsFalseIfValueAlreadyExists(int preloadCount)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var validationStack = new TestValidationStack();
|
||||||
|
var model = "This is a value";
|
||||||
|
|
||||||
|
PreLoad(preloadCount, validationStack);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.True(validationStack.Push(model));
|
||||||
|
Assert.False(validationStack.Push(model));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0)]
|
||||||
|
[InlineData(5)]
|
||||||
|
[InlineData(ValidationStack.CutOff + 1)]
|
||||||
|
public void Pop_RemovesValueFromTheStack(int preloadCount)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var validationStack = new TestValidationStack();
|
||||||
|
var model = "This is a value";
|
||||||
|
|
||||||
|
PreLoad(preloadCount, validationStack);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
validationStack.Push(model);
|
||||||
|
validationStack.Pop(model);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(validationStack.Contains(model));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0)]
|
||||||
|
[InlineData(5)]
|
||||||
|
[InlineData(ValidationStack.CutOff + 1)]
|
||||||
|
public void Pop_DoesNotThrowIfValueIsNull(int preloadCount)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var validationStack = new TestValidationStack();
|
||||||
|
|
||||||
|
PreLoad(preloadCount, validationStack);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
// Poping null when it's not there must not throw
|
||||||
|
validationStack.Pop(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PushingMoreThanCutOffElements_SwitchesToHashSet()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var size = ValidationStack.CutOff + 1;
|
||||||
|
|
||||||
|
var validationStack = new TestValidationStack();
|
||||||
|
var models = new List<Model>();
|
||||||
|
for (var i = 0; i < size; i++)
|
||||||
|
{
|
||||||
|
models.Add(new Model { Position = i });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
foreach (var model in models)
|
||||||
|
{
|
||||||
|
validationStack.Push(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(size, validationStack.Count);
|
||||||
|
|
||||||
|
models.Reverse();
|
||||||
|
foreach (var model in models)
|
||||||
|
{
|
||||||
|
validationStack.Pop(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(0, validationStack.Count);
|
||||||
|
Assert.True(validationStack.UsingHashSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PreLoad(int preloadCount, ValidationStack stack)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < preloadCount; i++)
|
||||||
|
{
|
||||||
|
stack.Push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Model
|
||||||
|
{
|
||||||
|
public int Position { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestValidationStack : ValidationStack
|
||||||
|
{
|
||||||
|
public bool Contains(object model)
|
||||||
|
{
|
||||||
|
if (HashSet != null)
|
||||||
|
{
|
||||||
|
return HashSet.Contains(model);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return List.Contains(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UsingHashSet()
|
||||||
|
{
|
||||||
|
return HashSet != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue