Skip to content

Commit 0c09859

Browse files
Optimize multiple attributes overwrite detection via SimpleStringIntDict
1 parent e0a4f72 commit 0c09859

File tree

1 file changed

+109
-32
lines changed

1 file changed

+109
-32
lines changed

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

Lines changed: 109 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Diagnostics;
7+
using System.Diagnostics.CodeAnalysis;
78
using System.Runtime.CompilerServices;
89
using Microsoft.AspNetCore.Components.Profiling;
910
using Microsoft.AspNetCore.Components.RenderTree;
@@ -28,7 +29,7 @@ public sealed class RenderTreeBuilder : IDisposable
2829
private readonly Stack<int> _openElementIndices = new Stack<int>();
2930
private RenderTreeFrameType? _lastNonAttributeFrameType;
3031
private bool _hasSeenAddMultipleAttributes;
31-
private Dictionary<string, int>? _seenAttributeNames;
32+
private SimpleStringIntDict? _seenAttributeNames;
3233

3334
/// <summary>
3435
/// The reserved parameter name used for supplying child content.
@@ -782,41 +783,39 @@ internal void ProcessDuplicateAttributes(int first)
782783
}
783784

784785
// Now that we've found the last attribute, we can iterate backwards and process duplicates.
785-
var seenAttributeNames = (_seenAttributeNames ??= new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase));
786+
var seenAttributeNames = (_seenAttributeNames ??= new SimpleStringIntDict());
786787
for (var i = last; i >= first; i--)
787788
{
788789
ref var frame = ref buffer[i];
789790
Debug.Assert(frame.FrameType == RenderTreeFrameType.Attribute, $"Frame type is {frame.FrameType} at {i}");
790791

791-
if (!seenAttributeNames.TryGetValue(frame.AttributeName, out var index))
792+
if (!seenAttributeNames.TryAdd(frame.AttributeName, i, out var index))
792793
{
793-
// This is the first time seeing this attribute name. Add to the dictionary and move on.
794-
seenAttributeNames.Add(frame.AttributeName, i);
795-
}
796-
else if (index < i)
797-
{
798-
// This attribute is overriding a "silent frame" where we didn't create a frame for an AddAttribute call.
799-
// This is the case for a null event handler, or bool false value.
800-
//
801-
// We need to update our tracking, in case the attribute appeared 3 or more times.
802-
seenAttributeNames[frame.AttributeName] = i;
803-
}
804-
else if (index > i)
805-
{
806-
// This attribute has been overridden. For now, blank out its name to *mark* it. We'll do a pass
807-
// later to wipe it out.
808-
frame = default;
809-
}
810-
else
811-
{
812-
// OK so index == i. How is that possible? Well it's possible for a "silent frame" immediately
813-
// followed by setting the same attribute. Think of it this way, when we create a "silent frame"
814-
// we have to track that attribute name with *some* index.
815-
//
816-
// The only index value we can safely use is _entries.Count (next available). This is fine because
817-
// we never use these indexes to look stuff up, only for comparison.
818-
//
819-
// That gets you here, and there's no action to take.
794+
if (index < i)
795+
{
796+
// This attribute is overriding a "silent frame" where we didn't create a frame for an AddAttribute call.
797+
// This is the case for a null event handler, or bool false value.
798+
//
799+
// We need to update our tracking, in case the attribute appeared 3 or more times.
800+
seenAttributeNames.Replace(frame.AttributeName, index);
801+
}
802+
else if (index > i)
803+
{
804+
// This attribute has been overridden. For now, blank out its name to *mark* it. We'll do a pass
805+
// later to wipe it out.
806+
frame = default;
807+
}
808+
else
809+
{
810+
// OK so index == i. How is that possible? Well it's possible for a "silent frame" immediately
811+
// followed by setting the same attribute. Think of it this way, when we create a "silent frame"
812+
// we have to track that attribute name with *some* index.
813+
//
814+
// The only index value we can safely use is _entries.Count (next available). This is fine because
815+
// we never use these indexes to look stuff up, only for comparison.
816+
//
817+
// That gets you here, and there's no action to take.
818+
}
820819
}
821820
}
822821

@@ -856,8 +855,8 @@ internal void TrackAttributeName(string name)
856855
return;
857856
}
858857

859-
var seenAttributeNames = (_seenAttributeNames ??= new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase));
860-
seenAttributeNames[name] = _entries.Count; // See comment in ProcessAttributes for why this is OK.
858+
var seenAttributeNames = (_seenAttributeNames ??= new SimpleStringIntDict());
859+
seenAttributeNames.TryAdd(name, _entries.Count, out _); // See comment in ProcessAttributes for why this is OK.
861860
}
862861

863862
void IDisposable.Dispose()
@@ -879,5 +878,83 @@ private static void ProfilingStart([CallerMemberName] string? name = null)
879878
[Conditional("Profile_RenderTreeBuilder")]
880879
private static void ProfilingEnd([CallerMemberName] string? name = null)
881880
=> ComponentsProfiling.Instance.End(name);
881+
882+
private class SimpleStringIntDict
883+
{
884+
private string[] _keys = new string[100];
885+
private int[] _values = new int[100];
886+
887+
public bool TryAdd(string key, int value, out int existingValue)
888+
{
889+
if (TryFindIndex(key, out var index))
890+
{
891+
existingValue = _values[index];
892+
return false;
893+
}
894+
else if (index >= 0)
895+
{
896+
_keys[index] = key;
897+
_values[index] = value;
898+
existingValue = default;
899+
return true;
900+
}
901+
else
902+
{
903+
throw new InvalidOperationException("Storage is full");
904+
}
905+
}
906+
907+
public void Replace(string key, int value)
908+
{
909+
if (TryFindIndex(key, out var index))
910+
{
911+
_values[index] = value;
912+
}
913+
else
914+
{
915+
throw new InvalidOperationException("Key not found");
916+
}
917+
}
918+
919+
public void Clear()
920+
{
921+
Array.Clear(_keys, 0, _keys.Length);
922+
Array.Clear(_values, 0, _values.Length);
923+
}
924+
925+
private bool TryFindIndex(string key, out int existingIndexOrInsertionPosition)
926+
{
927+
var numBuckets = _keys.Length;
928+
var startIndex = key.Length == 0 ? 0 : char.ToLowerInvariant(key[key.Length / 2]);
929+
startIndex = startIndex * 31 + char.ToLowerInvariant(key[0]);
930+
startIndex = startIndex % numBuckets;
931+
var candidateIndex = startIndex;
932+
933+
do
934+
{
935+
var candidateKey = _keys[candidateIndex];
936+
if (candidateKey == null)
937+
{
938+
existingIndexOrInsertionPosition = candidateIndex;
939+
return false;
940+
}
941+
942+
if (string.Equals(candidateKey, key, StringComparison.OrdinalIgnoreCase))
943+
{
944+
existingIndexOrInsertionPosition = candidateIndex;
945+
return true;
946+
}
947+
948+
if (++candidateIndex >= numBuckets)
949+
{
950+
candidateIndex = 0;
951+
}
952+
}
953+
while (candidateIndex != startIndex);
954+
955+
existingIndexOrInsertionPosition = -1;
956+
return false;
957+
}
958+
}
882959
}
883960
}

0 commit comments

Comments
 (0)