Use a prefix tree as a backing store for ModelStateDictionary #4108
Conversation
cc @rynowak |
@@ -20,7 +22,10 @@ public class ModelStateDictionary : IDictionary<string, ModelStateEntry> | |||
/// </summary> | |||
public static readonly int DefaultMaxAllowedErrors = 200; | |||
|
|||
private readonly Dictionary<string, ModelStateEntry> _innerDictionary; | |||
private static readonly StringSegment EmptyStringSegment = new StringSegment(buffer: string.Empty); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
StringSegment
is a struct, there's really no benefit to caching this.
⌚ |
var currentRoot = _root; | ||
var previousIndex = 0; | ||
int index; | ||
while ((index = key.IndexOfAny(Delimiters, previousIndex)) != -1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be great to show sample input in comments
09ac7b4
to
11f7f26
Compare
Updated. Working on adding more tests |
} | ||
|
||
/// <inheritdoc /> | ||
public bool Contains(KeyValuePair<string, ModelStateEntry> item) | ||
bool ICollection<KeyValuePair<string, ModelStateEntry>>.Contains(KeyValuePair<string, ModelStateEntry> item) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how to go about this - the equality comparison for item.Value
will always return false since we create a copy of the one provided to us in Add or indexer. Should we just have this return false?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same goes for Remove(KeyValuePair<string, ModelStateEntry>)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I guess it's a bit of an issue that there's no deep equality for MSE.
IMO the better thing to do is throw an exception. (NotSupportedException
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
11f7f26
to
b94dc3e
Compare
throw new ArgumentOutOfRangeException(nameof(arrayIndex)); | ||
} | ||
|
||
foreach (var item in this) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could do this recursively to avoid the enumerator allocation. Not sure how much we care about CopyTo
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not worried about it.
⌚ for a few smaller comments |
b94dc3e
to
936fce1
Compare
searchKey.Offset, | ||
searchKey.Length, | ||
StringComparison.OrdinalIgnoreCase); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In previous iteration, property "AB"
would come before property "Z"
. Not sure how much difference the intrinsic order makes but why place shorter property names first?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Put another way, why not check the length only if string.Compare()
returns 0
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's going to be faster to compare lengths first than run through the string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's going to be faster to compare lengths first than run through the string.
Might only be visible in BadRequest(ModelState)
responses and where actions use the enumerators. But the (early) optimization isn't worth this bizarre order.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We agreed offline to fix this up i.e. make the order sane. Correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm leaving this as is. The dictionary has no guarantees of ordering and I think it's fine the way we have things now. If we need ordering for BadRequest
we should change it there.
⌚ for a couple more doc comments. And perhaps |
Larger question: With this change being allocation-neutral, is it worth going ahead? In particular is there a RPS improvement or does an execution hotspot go away? If not, I'm a fan of the improved reliability of the |
Yes. This change is focused on reducing the cost of things like |
If dotTrace should show a solid improvement, suggest measuring. |
Sampling profiles of Before
After
|
911a32c
to
fdc9b7f
Compare
Updated |
fdc9b7f
to
041ce28
Compare
|
||
using System.Collections.Generic; | ||
|
||
namespace Microsoft.AspNetCore.Mvc.ModelBinding |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👏 for fixing the filename
⌚ for #4108 (diff) resolution. |
041ce28
to
52625ba
Compare
Updated. |
and please file the prefixed key follow-up issue. |
No description provided.