Skip to content

Commit 7e11c3c

Browse files
Cache parameter writer lookups (#24536)
1 parent 38e166f commit 7e11c3c

File tree

1 file changed

+40
-7
lines changed

1 file changed

+40
-7
lines changed

src/Components/Components/src/Reflection/ComponentProperties.cs

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public static void SetProperties(in ParameterView parameters, object target)
4141
foreach (var parameter in parameters)
4242
{
4343
var parameterName = parameter.Name;
44-
if (!writers.WritersByName.TryGetValue(parameterName, out var writer))
44+
if (!writers.TryGetValue(parameterName, out var writer))
4545
{
4646
// Case 1: There is nowhere to put this value.
4747
ThrowForUnknownIncomingParameterName(targetType, parameterName);
@@ -82,7 +82,7 @@ public static void SetProperties(in ParameterView parameters, object target)
8282
isCaptureUnmatchedValuesParameterSetExplicitly = true;
8383
}
8484

85-
if (writers.WritersByName.TryGetValue(parameterName, out var writer))
85+
if (writers.TryGetValue(parameterName, out var writer))
8686
{
8787
if (!writer.Cascading && parameter.Cascading)
8888
{
@@ -245,9 +245,15 @@ private static void ThrowForInvalidCaptureUnmatchedValuesParameterType(Type targ
245245

246246
private class WritersForType
247247
{
248+
private const int MaxCachedWriterLookups = 100;
249+
private readonly Dictionary<string, IPropertySetter> _underlyingWriters;
250+
private readonly ConcurrentDictionary<string, IPropertySetter?> _referenceEqualityWritersCache;
251+
248252
public WritersForType(Type targetType)
249253
{
250-
WritersByName = new Dictionary<string, IPropertySetter>(StringComparer.OrdinalIgnoreCase);
254+
_underlyingWriters = new Dictionary<string, IPropertySetter>(StringComparer.OrdinalIgnoreCase);
255+
_referenceEqualityWritersCache = new ConcurrentDictionary<string, IPropertySetter?>(ReferenceEqualityComparer.Instance);
256+
251257
foreach (var propertyInfo in GetCandidateBindableProperties(targetType))
252258
{
253259
var parameterAttribute = propertyInfo.GetCustomAttribute<ParameterAttribute>();
@@ -267,14 +273,14 @@ public WritersForType(Type targetType)
267273

268274
var propertySetter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo, cascading: cascadingParameterAttribute != null);
269275

270-
if (WritersByName.ContainsKey(propertyName))
276+
if (_underlyingWriters.ContainsKey(propertyName))
271277
{
272278
throw new InvalidOperationException(
273279
$"The type '{targetType.FullName}' declares more than one parameter matching the " +
274280
$"name '{propertyName.ToLowerInvariant()}'. Parameter names are case-insensitive and must be unique.");
275281
}
276282

277-
WritersByName.Add(propertyName, propertySetter);
283+
_underlyingWriters.Add(propertyName, propertySetter);
278284

279285
if (parameterAttribute != null && parameterAttribute.CaptureUnmatchedValues)
280286
{
@@ -298,11 +304,38 @@ public WritersForType(Type targetType)
298304
}
299305
}
300306

301-
public Dictionary<string, IPropertySetter> WritersByName { get; }
302-
303307
public IPropertySetter? CaptureUnmatchedValuesWriter { get; }
304308

305309
public string? CaptureUnmatchedValuesPropertyName { get; }
310+
311+
public bool TryGetValue(string parameterName, [MaybeNullWhen(false)] out IPropertySetter writer)
312+
{
313+
// In intensive parameter-passing scenarios, one of the most expensive things we do is the
314+
// lookup from parameterName to writer. Pre-5.0 that was because of the string hashing.
315+
// To optimize this, we now have a cache in front of the lookup which is keyed by parameterName's
316+
// object identity (not its string hash). So in most cases we can resolve the lookup without
317+
// having to hash the string. We only fall back on hashing the string if the cache gets full,
318+
// which would only be in very unusual situations because components don't typically have many
319+
// parameters, and the parameterName strings usually come from compile-time constants.
320+
if (!_referenceEqualityWritersCache.TryGetValue(parameterName, out writer))
321+
{
322+
_underlyingWriters.TryGetValue(parameterName, out writer);
323+
324+
// Note that because we're not locking around this, it's possible we might
325+
// actually write more than MaxCachedWriterLookups entries due to concurrent
326+
// writes. However this won't cause any problems.
327+
// Also note that the value we're caching might be 'null'. It's valid to cache
328+
// lookup misses just as much as hits, since then we can more quickly identify
329+
// incoming values that don't have a corresponding writer and thus will end up
330+
// being passed as catch-all parameter values.
331+
if (_referenceEqualityWritersCache.Count < MaxCachedWriterLookups)
332+
{
333+
_referenceEqualityWritersCache.TryAdd(parameterName, writer);
334+
}
335+
}
336+
337+
return writer != null;
338+
}
306339
}
307340
}
308341
}

0 commit comments

Comments
 (0)