diff --git a/src/Components/Components/src/Rendering/RenderTreeBuilder.cs b/src/Components/Components/src/Rendering/RenderTreeBuilder.cs index c3d1edcd50f2..d7b78f4cae61 100644 --- a/src/Components/Components/src/Rendering/RenderTreeBuilder.cs +++ b/src/Components/Components/src/Rendering/RenderTreeBuilder.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Components.RenderTree; namespace Microsoft.AspNetCore.Components.Rendering @@ -707,41 +706,40 @@ internal void ProcessDuplicateAttributes(int first) } // Now that we've found the last attribute, we can iterate backwards and process duplicates. - var seenAttributeNames = (_seenAttributeNames ??= new Dictionary(StringComparer.OrdinalIgnoreCase)); + var seenAttributeNames = (_seenAttributeNames ??= new Dictionary(SimplifiedStringHashComparer.Instance)); for (var i = last; i >= first; i--) { ref var frame = ref buffer[i]; Debug.Assert(frame.FrameTypeField == RenderTreeFrameType.Attribute, $"Frame type is {frame.FrameTypeField} at {i}"); - if (!seenAttributeNames.TryGetValue(frame.AttributeNameField, out var index)) + if (!seenAttributeNames.TryAdd(frame.AttributeNameField, i)) { - // This is the first time seeing this attribute name. Add to the dictionary and move on. - seenAttributeNames.Add(frame.AttributeNameField, i); - } - else if (index < i) - { - // This attribute is overriding a "silent frame" where we didn't create a frame for an AddAttribute call. - // This is the case for a null event handler, or bool false value. - // - // We need to update our tracking, in case the attribute appeared 3 or more times. - seenAttributeNames[frame.AttributeNameField] = i; - } - else if (index > i) - { - // This attribute has been overridden. For now, blank out its name to *mark* it. We'll do a pass - // later to wipe it out. - frame = default; - } - else - { - // OK so index == i. How is that possible? Well it's possible for a "silent frame" immediately - // followed by setting the same attribute. Think of it this way, when we create a "silent frame" - // we have to track that attribute name with *some* index. - // - // The only index value we can safely use is _entries.Count (next available). This is fine because - // we never use these indexes to look stuff up, only for comparison. - // - // That gets you here, and there's no action to take. + var index = seenAttributeNames[frame.AttributeNameField]; + if (index < i) + { + // This attribute is overriding a "silent frame" where we didn't create a frame for an AddAttribute call. + // This is the case for a null event handler, or bool false value. + // + // We need to update our tracking, in case the attribute appeared 3 or more times. + seenAttributeNames[frame.AttributeNameField] = i; + } + else if (index > i) + { + // This attribute has been overridden. For now, blank out its name to *mark* it. We'll do a pass + // later to wipe it out. + frame = default; + } + else + { + // OK so index == i. How is that possible? Well it's possible for a "silent frame" immediately + // followed by setting the same attribute. Think of it this way, when we create a "silent frame" + // we have to track that attribute name with *some* index. + // + // The only index value we can safely use is _entries.Count (next available). This is fine because + // we never use these indexes to look stuff up, only for comparison. + // + // That gets you here, and there's no action to take. + } } } @@ -780,7 +778,7 @@ internal void TrackAttributeName(string name) return; } - var seenAttributeNames = (_seenAttributeNames ??= new Dictionary(StringComparer.OrdinalIgnoreCase)); + var seenAttributeNames = (_seenAttributeNames ??= new Dictionary(SimplifiedStringHashComparer.Instance)); seenAttributeNames[name] = _entries.Count; // See comment in ProcessAttributes for why this is OK. } diff --git a/src/Components/Components/src/Rendering/SimplifiedStringHashComparer.cs b/src/Components/Components/src/Rendering/SimplifiedStringHashComparer.cs new file mode 100644 index 000000000000..ecdb7cc8dd2f --- /dev/null +++ b/src/Components/Components/src/Rendering/SimplifiedStringHashComparer.cs @@ -0,0 +1,47 @@ +// 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; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Components.Rendering +{ + /// + /// This comparer is optimized for use with dictionaries where the great majority of insertions/lookups + /// don't match existing entries. For example, when building a dictionary of almost entirely unique keys. + /// It's faster than the normal string comparer in this case because it doesn't use string.GetHashCode, + /// and hence doesn't have to consider every character in the string. + /// + /// This primary scenario is , which needs + /// to detect when one attribute is overriding another, but in the vast majority of cases attributes don't + /// actually override each other. + /// + internal class SimplifiedStringHashComparer : IEqualityComparer + { + public readonly static SimplifiedStringHashComparer Instance = new SimplifiedStringHashComparer(); + + public bool Equals(string? x, string? y) + { + return string.Equals(x, y, StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode(string key) + { + var keyLength = key.Length; + if (keyLength > 0) + { + // Consider just the length and middle and last characters. + // This will produce a distinct result for a sufficiently large + // proportion of attribute names. + return unchecked( + char.ToLowerInvariant(key[keyLength - 1]) + + 31 * char.ToLowerInvariant(key[keyLength / 2]) + + 961 * keyLength); + } + else + { + return default; + } + } + } +} diff --git a/src/Components/Components/test/Rendering/SimplifiedStringHashComparerTest.cs b/src/Components/Components/test/Rendering/SimplifiedStringHashComparerTest.cs new file mode 100644 index 000000000000..7fcd70743ebd --- /dev/null +++ b/src/Components/Components/test/Rendering/SimplifiedStringHashComparerTest.cs @@ -0,0 +1,24 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Components.Rendering +{ + public class SimplifiedStringHashComparerTest + { + [Fact] + public void EqualityIsCaseInsensitive() + { + Assert.True(SimplifiedStringHashComparer.Instance.Equals("abc", "ABC")); + } + + [Fact] + public void HashCodesAreCaseInsensitive() + { + var hash1 = SimplifiedStringHashComparer.Instance.GetHashCode("abc"); + var hash2 = SimplifiedStringHashComparer.Instance.GetHashCode("ABC"); + Assert.Equal(hash1, hash2); + } + } +}