Skip to content

Commit be6200d

Browse files
Make SetParameterProperties free of allocations
1 parent 5044c52 commit be6200d

File tree

2 files changed

+78
-42
lines changed

2 files changed

+78
-42
lines changed

src/Components/src/Microsoft.AspNetCore.Components/Microsoft.AspNetCore.Components.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>netstandard2.0</TargetFramework>
55
<Description>Components feature for ASP.NET Core.</Description>
6+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
67
</PropertyGroup>
78

89
<ItemGroup>

src/Components/src/Microsoft.AspNetCore.Components/ParameterCollectionExtensions.cs

Lines changed: 76 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System;
66
using System.Collections.Concurrent;
77
using System.Collections.Generic;
8-
using System.Linq;
98
using System.Reflection;
109

1110
namespace Microsoft.AspNetCore.Components
@@ -17,15 +16,16 @@ public static class ParameterCollectionExtensions
1716
{
1817
private const BindingFlags _bindablePropertyFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase;
1918

20-
private readonly static IDictionary<Type, IDictionary<string, IPropertySetter>> _cachedParameterWriters = new ConcurrentDictionary<Type, IDictionary<string, IPropertySetter>>();
19+
private readonly static ConcurrentDictionary<Type, WritersForType> _cachedWritersByType
20+
= new ConcurrentDictionary<Type, WritersForType>();
2121

2222
/// <summary>
23-
/// Iterates through the <see cref="ParameterCollection"/>, assigning each parameter
24-
/// to a property of the same name on <paramref name="target"/>.
23+
/// For each parameter property on <paramref name="target"/>, updates its value to
24+
/// match the corresponding entry in the <see cref="ParameterCollection"/>.
2525
/// </summary>
2626
/// <param name="parameterCollection">The <see cref="ParameterCollection"/>.</param>
2727
/// <param name="target">An object that has a public writable property matching each parameter's name and type.</param>
28-
public static void SetParameterProperties(
28+
public unsafe static void SetParameterProperties(
2929
in this ParameterCollection parameterCollection,
3030
object target)
3131
{
@@ -35,26 +35,36 @@ public static void SetParameterProperties(
3535
}
3636

3737
var targetType = target.GetType();
38-
if (!_cachedParameterWriters.TryGetValue(targetType, out var parameterWriters))
38+
if (!_cachedWritersByType.TryGetValue(targetType, out var writers))
3939
{
40-
parameterWriters = CreateParameterWriters(targetType);
41-
_cachedParameterWriters[targetType] = parameterWriters;
40+
writers = new WritersForType(targetType);
41+
_cachedWritersByType[targetType] = writers;
4242
}
4343

44-
var localParameterWriter = parameterWriters.Values.ToList();
44+
// We only want to iterate through the parameterCollection once, and by the end of it,
45+
// need to have tracked which of the parameter properties haven't yet been written.
46+
// To avoid allocating any list/dictionary to track that, here we stackalloc an array
47+
// of flags and set them based on the indices of the writers we use.
48+
var numWriters = writers.WritersByIndex.Count;
49+
var numUsedWriters = 0;
50+
51+
// TODO: Once we're able to move to netstandard2.1, this can be changed to be
52+
// a Span<bool> and then the enclosing method no longer needs to be 'unsafe'
53+
bool* usageFlags = stackalloc bool[numWriters];
4554

4655
foreach (var parameter in parameterCollection)
4756
{
4857
var parameterName = parameter.Name;
49-
if (!parameterWriters.TryGetValue(parameterName, out var parameterWriter))
58+
if (!writers.WritersByName.TryGetValue(parameterName, out var writerWithIndex))
5059
{
5160
ThrowForUnknownIncomingParameterName(targetType, parameterName);
5261
}
5362

5463
try
5564
{
56-
parameterWriter.SetValue(target, parameter.Value);
57-
localParameterWriter.Remove(parameterWriter);
65+
writerWithIndex.Writer.SetValue(target, parameter.Value);
66+
usageFlags[writerWithIndex.Index] = true;
67+
numUsedWriters++;
5868
}
5969
catch (Exception ex)
6070
{
@@ -64,44 +74,27 @@ public static void SetParameterProperties(
6474
}
6575
}
6676

67-
foreach (var nonUsedParameter in localParameterWriter)
68-
{
69-
nonUsedParameter.SetDefaultValue(target);
70-
}
71-
}
72-
73-
internal static IEnumerable<PropertyInfo> GetCandidateBindableProperties(Type targetType)
74-
=> MemberAssignment.GetPropertiesIncludingInherited(targetType, _bindablePropertyFlags);
75-
76-
private static IDictionary<string, IPropertySetter> CreateParameterWriters(Type targetType)
77-
{
78-
var result = new Dictionary<string, IPropertySetter>(StringComparer.OrdinalIgnoreCase);
79-
80-
foreach (var propertyInfo in GetCandidateBindableProperties(targetType))
77+
// Now we can determine whether any writers have not been used, and if there are
78+
// some unused ones, find them.
79+
for (var index = 0; numUsedWriters < numWriters; index++)
8180
{
82-
var shouldCreateWriter = propertyInfo.IsDefined(typeof(ParameterAttribute))
83-
|| propertyInfo.IsDefined(typeof(CascadingParameterAttribute));
84-
if (!shouldCreateWriter)
81+
if (index >= numWriters)
8582
{
86-
continue;
83+
// This should not be possible
84+
throw new InvalidOperationException("Ran out of writers before marking them all as used.");
8785
}
8886

89-
var propertySetter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo);
90-
91-
var propertyName = propertyInfo.Name;
92-
if (result.ContainsKey(propertyName))
87+
if (!usageFlags[index])
9388
{
94-
throw new InvalidOperationException(
95-
$"The type '{targetType.FullName}' declares more than one parameter matching the " +
96-
$"name '{propertyName.ToLowerInvariant()}'. Parameter names are case-insensitive and must be unique.");
89+
writers.WritersByIndex[index].SetDefaultValue(target);
90+
numUsedWriters++;
9791
}
98-
99-
result.Add(propertyName, propertySetter);
10092
}
101-
102-
return result;
10393
}
10494

95+
internal static IEnumerable<PropertyInfo> GetCandidateBindableProperties(Type targetType)
96+
=> MemberAssignment.GetPropertiesIncludingInherited(targetType, _bindablePropertyFlags);
97+
10598
private static void ThrowForUnknownIncomingParameterName(Type targetType, string parameterName)
10699
{
107100
// We know we're going to throw by this stage, so it doesn't matter that the following
@@ -129,5 +122,47 @@ private static void ThrowForUnknownIncomingParameterName(Type targetType, string
129122
$"matching the name '{parameterName}'.");
130123
}
131124
}
125+
126+
class WritersForType
127+
{
128+
public Dictionary<string, (int Index, IPropertySetter Writer)> WritersByName { get; }
129+
public List<IPropertySetter> WritersByIndex { get; }
130+
131+
public WritersForType(Type targetType)
132+
{
133+
var propertySettersByName = new Dictionary<string, IPropertySetter>(StringComparer.OrdinalIgnoreCase);
134+
foreach (var propertyInfo in GetCandidateBindableProperties(targetType))
135+
{
136+
var shouldCreateWriter = propertyInfo.IsDefined(typeof(ParameterAttribute))
137+
|| propertyInfo.IsDefined(typeof(CascadingParameterAttribute));
138+
if (!shouldCreateWriter)
139+
{
140+
continue;
141+
}
142+
143+
var propertySetter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo);
144+
145+
var propertyName = propertyInfo.Name;
146+
if (propertySettersByName.ContainsKey(propertyName))
147+
{
148+
throw new InvalidOperationException(
149+
$"The type '{targetType.FullName}' declares more than one parameter matching the " +
150+
$"name '{propertyName.ToLowerInvariant()}'. Parameter names are case-insensitive and must be unique.");
151+
}
152+
153+
propertySettersByName.Add(propertyName, propertySetter);
154+
}
155+
156+
// Now we know all the entries, construct the resulting list/dictionary
157+
// with well-defined indices
158+
WritersByIndex = new List<IPropertySetter>();
159+
WritersByName = new Dictionary<string, (int, IPropertySetter)>(StringComparer.OrdinalIgnoreCase);
160+
foreach (var pair in propertySettersByName)
161+
{
162+
WritersByName.Add(pair.Key, (WritersByIndex.Count, pair.Value));
163+
WritersByIndex.Add(pair.Value);
164+
}
165+
}
166+
}
132167
}
133168
}

0 commit comments

Comments
 (0)