Skip to content

Commit f058274

Browse files
Speed up multiple attributes overwrite detection. Fixes #24467 (#24561)
1 parent d99644e commit f058274

File tree

3 files changed

+100
-31
lines changed

3 files changed

+100
-31
lines changed

src/Components/Components/src/Rendering/RenderTreeBuilder.cs

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Diagnostics;
7-
using System.Runtime.CompilerServices;
87
using Microsoft.AspNetCore.Components.RenderTree;
98

109
namespace Microsoft.AspNetCore.Components.Rendering
@@ -707,41 +706,40 @@ internal void ProcessDuplicateAttributes(int first)
707706
}
708707

709708
// Now that we've found the last attribute, we can iterate backwards and process duplicates.
710-
var seenAttributeNames = (_seenAttributeNames ??= new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase));
709+
var seenAttributeNames = (_seenAttributeNames ??= new Dictionary<string, int>(SimplifiedStringHashComparer.Instance));
711710
for (var i = last; i >= first; i--)
712711
{
713712
ref var frame = ref buffer[i];
714713
Debug.Assert(frame.FrameTypeField == RenderTreeFrameType.Attribute, $"Frame type is {frame.FrameTypeField} at {i}");
715714

716-
if (!seenAttributeNames.TryGetValue(frame.AttributeNameField, out var index))
715+
if (!seenAttributeNames.TryAdd(frame.AttributeNameField, i))
717716
{
718-
// This is the first time seeing this attribute name. Add to the dictionary and move on.
719-
seenAttributeNames.Add(frame.AttributeNameField, i);
720-
}
721-
else if (index < i)
722-
{
723-
// This attribute is overriding a "silent frame" where we didn't create a frame for an AddAttribute call.
724-
// This is the case for a null event handler, or bool false value.
725-
//
726-
// We need to update our tracking, in case the attribute appeared 3 or more times.
727-
seenAttributeNames[frame.AttributeNameField] = i;
728-
}
729-
else if (index > i)
730-
{
731-
// This attribute has been overridden. For now, blank out its name to *mark* it. We'll do a pass
732-
// later to wipe it out.
733-
frame = default;
734-
}
735-
else
736-
{
737-
// OK so index == i. How is that possible? Well it's possible for a "silent frame" immediately
738-
// followed by setting the same attribute. Think of it this way, when we create a "silent frame"
739-
// we have to track that attribute name with *some* index.
740-
//
741-
// The only index value we can safely use is _entries.Count (next available). This is fine because
742-
// we never use these indexes to look stuff up, only for comparison.
743-
//
744-
// That gets you here, and there's no action to take.
717+
var index = seenAttributeNames[frame.AttributeNameField];
718+
if (index < i)
719+
{
720+
// This attribute is overriding a "silent frame" where we didn't create a frame for an AddAttribute call.
721+
// This is the case for a null event handler, or bool false value.
722+
//
723+
// We need to update our tracking, in case the attribute appeared 3 or more times.
724+
seenAttributeNames[frame.AttributeNameField] = i;
725+
}
726+
else if (index > i)
727+
{
728+
// This attribute has been overridden. For now, blank out its name to *mark* it. We'll do a pass
729+
// later to wipe it out.
730+
frame = default;
731+
}
732+
else
733+
{
734+
// OK so index == i. How is that possible? Well it's possible for a "silent frame" immediately
735+
// followed by setting the same attribute. Think of it this way, when we create a "silent frame"
736+
// we have to track that attribute name with *some* index.
737+
//
738+
// The only index value we can safely use is _entries.Count (next available). This is fine because
739+
// we never use these indexes to look stuff up, only for comparison.
740+
//
741+
// That gets you here, and there's no action to take.
742+
}
745743
}
746744
}
747745

@@ -780,7 +778,7 @@ internal void TrackAttributeName(string name)
780778
return;
781779
}
782780

783-
var seenAttributeNames = (_seenAttributeNames ??= new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase));
781+
var seenAttributeNames = (_seenAttributeNames ??= new Dictionary<string, int>(SimplifiedStringHashComparer.Instance));
784782
seenAttributeNames[name] = _entries.Count; // See comment in ProcessAttributes for why this is OK.
785783
}
786784

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
7+
namespace Microsoft.AspNetCore.Components.Rendering
8+
{
9+
/// <summary>
10+
/// This comparer is optimized for use with dictionaries where the great majority of insertions/lookups
11+
/// don't match existing entries. For example, when building a dictionary of almost entirely unique keys.
12+
/// It's faster than the normal string comparer in this case because it doesn't use string.GetHashCode,
13+
/// and hence doesn't have to consider every character in the string.
14+
///
15+
/// This primary scenario is <see cref="RenderTreeBuilder.ProcessDuplicateAttributes(int)"/>, which needs
16+
/// to detect when one attribute is overriding another, but in the vast majority of cases attributes don't
17+
/// actually override each other.
18+
/// </summary>
19+
internal class SimplifiedStringHashComparer : IEqualityComparer<string>
20+
{
21+
public readonly static SimplifiedStringHashComparer Instance = new SimplifiedStringHashComparer();
22+
23+
public bool Equals(string? x, string? y)
24+
{
25+
return string.Equals(x, y, StringComparison.OrdinalIgnoreCase);
26+
}
27+
28+
public int GetHashCode(string key)
29+
{
30+
var keyLength = key.Length;
31+
if (keyLength > 0)
32+
{
33+
// Consider just the length and middle and last characters.
34+
// This will produce a distinct result for a sufficiently large
35+
// proportion of attribute names.
36+
return unchecked(
37+
char.ToLowerInvariant(key[keyLength - 1])
38+
+ 31 * char.ToLowerInvariant(key[keyLength / 2])
39+
+ 961 * keyLength);
40+
}
41+
else
42+
{
43+
return default;
44+
}
45+
}
46+
}
47+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Xunit;
5+
6+
namespace Microsoft.AspNetCore.Components.Rendering
7+
{
8+
public class SimplifiedStringHashComparerTest
9+
{
10+
[Fact]
11+
public void EqualityIsCaseInsensitive()
12+
{
13+
Assert.True(SimplifiedStringHashComparer.Instance.Equals("abc", "ABC"));
14+
}
15+
16+
[Fact]
17+
public void HashCodesAreCaseInsensitive()
18+
{
19+
var hash1 = SimplifiedStringHashComparer.Instance.GetHashCode("abc");
20+
var hash2 = SimplifiedStringHashComparer.Instance.GetHashCode("ABC");
21+
Assert.Equal(hash1, hash2);
22+
}
23+
}
24+
}

0 commit comments

Comments
 (0)