Speed up multiple attributes overwrite detection. Fixes #24467 (#24561)

This commit is contained in:
Steve Sanderson 2020-08-05 16:39:36 +01:00 committed by GitHub
parent d99644ef9c
commit f05827484e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 100 additions and 31 deletions

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.RenderTree;
namespace Microsoft.AspNetCore.Components.Rendering namespace Microsoft.AspNetCore.Components.Rendering
@ -707,41 +706,40 @@ namespace Microsoft.AspNetCore.Components.Rendering
} }
// Now that we've found the last attribute, we can iterate backwards and process duplicates. // Now that we've found the last attribute, we can iterate backwards and process duplicates.
var seenAttributeNames = (_seenAttributeNames ??= new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)); var seenAttributeNames = (_seenAttributeNames ??= new Dictionary<string, int>(SimplifiedStringHashComparer.Instance));
for (var i = last; i >= first; i--) for (var i = last; i >= first; i--)
{ {
ref var frame = ref buffer[i]; ref var frame = ref buffer[i];
Debug.Assert(frame.FrameTypeField == RenderTreeFrameType.Attribute, $"Frame type is {frame.FrameTypeField} at {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. var index = seenAttributeNames[frame.AttributeNameField];
seenAttributeNames.Add(frame.AttributeNameField, i); if (index < 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.
// 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;
// We need to update our tracking, in case the attribute appeared 3 or more times. }
seenAttributeNames[frame.AttributeNameField] = i; else if (index > 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.
// This attribute has been overridden. For now, blank out its name to *mark* it. We'll do a pass frame = default;
// later to wipe it out. }
frame = default; else
} {
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"
// OK so index == i. How is that possible? Well it's possible for a "silent frame" immediately // we have to track that attribute name with *some* index.
// 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.
// 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.
// }
// That gets you here, and there's no action to take.
} }
} }
@ -780,7 +778,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
return; return;
} }
var seenAttributeNames = (_seenAttributeNames ??= new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)); var seenAttributeNames = (_seenAttributeNames ??= new Dictionary<string, int>(SimplifiedStringHashComparer.Instance));
seenAttributeNames[name] = _entries.Count; // See comment in ProcessAttributes for why this is OK. seenAttributeNames[name] = _entries.Count; // See comment in ProcessAttributes for why this is OK.
} }

View File

@ -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
{
/// <summary>
/// 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 <see cref="RenderTreeBuilder.ProcessDuplicateAttributes(int)"/>, which needs
/// to detect when one attribute is overriding another, but in the vast majority of cases attributes don't
/// actually override each other.
/// </summary>
internal class SimplifiedStringHashComparer : IEqualityComparer<string>
{
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;
}
}
}
}

View File

@ -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);
}
}
}