Make PropertySetter a concrete type (#25054)

* Make PropertySetter a concrete type

* Use the pattern from PropertyHelpers to set property values
* Tweaks to Blazor WebAssembly tests to allow running tests locally

* Update src/Components/Components/src/Reflection/IPropertySetter.cs
This commit is contained in:
Pranav K 2020-08-24 18:54:28 -07:00 committed by GitHub
parent db77380c84
commit 29cbf6e106
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 107 additions and 61 deletions

View File

@ -63,7 +63,7 @@ namespace Microsoft.AspNetCore.Components
( (
propertyName: property.Name, propertyName: property.Name,
propertyType: property.PropertyType, propertyType: property.PropertyType,
setter: MemberAssignment.CreatePropertySetter(type, property, cascading: false) setter: new PropertySetter(type, property)
)).ToArray(); )).ToArray();
return Initialize; return Initialize;

View File

@ -144,7 +144,7 @@ namespace Microsoft.AspNetCore.Components.Reflection
} }
} }
static void SetProperty(object target, IPropertySetter writer, string parameterName, object value) static void SetProperty(object target, PropertySetter writer, string parameterName, object value)
{ {
try try
{ {
@ -246,13 +246,13 @@ namespace Microsoft.AspNetCore.Components.Reflection
private class WritersForType private class WritersForType
{ {
private const int MaxCachedWriterLookups = 100; private const int MaxCachedWriterLookups = 100;
private readonly Dictionary<string, IPropertySetter> _underlyingWriters; private readonly Dictionary<string, PropertySetter> _underlyingWriters;
private readonly ConcurrentDictionary<string, IPropertySetter?> _referenceEqualityWritersCache; private readonly ConcurrentDictionary<string, PropertySetter?> _referenceEqualityWritersCache;
public WritersForType(Type targetType) public WritersForType(Type targetType)
{ {
_underlyingWriters = new Dictionary<string, IPropertySetter>(StringComparer.OrdinalIgnoreCase); _underlyingWriters = new Dictionary<string, PropertySetter>(StringComparer.OrdinalIgnoreCase);
_referenceEqualityWritersCache = new ConcurrentDictionary<string, IPropertySetter?>(ReferenceEqualityComparer.Instance); _referenceEqualityWritersCache = new ConcurrentDictionary<string, PropertySetter?>(ReferenceEqualityComparer.Instance);
foreach (var propertyInfo in GetCandidateBindableProperties(targetType)) foreach (var propertyInfo in GetCandidateBindableProperties(targetType))
{ {
@ -271,7 +271,10 @@ namespace Microsoft.AspNetCore.Components.Reflection
$"The type '{targetType.FullName}' declares a parameter matching the name '{propertyName}' that is not public. Parameters must be public."); $"The type '{targetType.FullName}' declares a parameter matching the name '{propertyName}' that is not public. Parameters must be public.");
} }
var propertySetter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo, cascading: cascadingParameterAttribute != null); var propertySetter = new PropertySetter(targetType, propertyInfo)
{
Cascading = cascadingParameterAttribute != null,
};
if (_underlyingWriters.ContainsKey(propertyName)) if (_underlyingWriters.ContainsKey(propertyName))
{ {
@ -298,17 +301,17 @@ namespace Microsoft.AspNetCore.Components.Reflection
ThrowForInvalidCaptureUnmatchedValuesParameterType(targetType, propertyInfo); ThrowForInvalidCaptureUnmatchedValuesParameterType(targetType, propertyInfo);
} }
CaptureUnmatchedValuesWriter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo, cascading: false); CaptureUnmatchedValuesWriter = new PropertySetter(targetType, propertyInfo);
CaptureUnmatchedValuesPropertyName = propertyInfo.Name; CaptureUnmatchedValuesPropertyName = propertyInfo.Name;
} }
} }
} }
public IPropertySetter? CaptureUnmatchedValuesWriter { get; } public PropertySetter? CaptureUnmatchedValuesWriter { get; }
public string? CaptureUnmatchedValuesPropertyName { get; } public string? CaptureUnmatchedValuesPropertyName { get; }
public bool TryGetValue(string parameterName, [MaybeNullWhen(false)] out IPropertySetter writer) public bool TryGetValue(string parameterName, [MaybeNullWhen(false)] out PropertySetter writer)
{ {
// In intensive parameter-passing scenarios, one of the most expensive things we do is the // In intensive parameter-passing scenarios, one of the most expensive things we do is the
// lookup from parameterName to writer. Pre-5.0 that was because of the string hashing. // lookup from parameterName to writer. Pre-5.0 that was because of the string hashing.

View File

@ -1,12 +1,55 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Reflection;
namespace Microsoft.AspNetCore.Components.Reflection namespace Microsoft.AspNetCore.Components.Reflection
{ {
internal interface IPropertySetter internal sealed class PropertySetter
{ {
bool Cascading { get; } private static readonly MethodInfo CallPropertySetterOpenGenericMethod =
typeof(PropertySetter).GetMethod(nameof(CallPropertySetter), BindingFlags.NonPublic | BindingFlags.Static)!;
void SetValue(object target, object value); private readonly Action<object, object> _setterDelegate;
public PropertySetter(Type targetType, PropertyInfo property)
{
if (property.SetMethod == null)
{
throw new InvalidOperationException($"Cannot provide a value for property " +
$"'{property.Name}' on type '{targetType.FullName}' because the property " +
$"has no setter.");
}
var setMethod = property.SetMethod;
var propertySetterAsAction =
setMethod.CreateDelegate(typeof(Action<,>).MakeGenericType(targetType, property.PropertyType));
var callPropertySetterClosedGenericMethod =
CallPropertySetterOpenGenericMethod.MakeGenericMethod(targetType, property.PropertyType);
_setterDelegate = (Action<object, object>)
callPropertySetterClosedGenericMethod.CreateDelegate(typeof(Action<object, object>), propertySetterAsAction);
}
public bool Cascading { get; init; }
public void SetValue(object target, object value) => _setterDelegate(target, value);
private static void CallPropertySetter<TTarget, TValue>(
Action<TTarget, TValue> setter,
object target,
object value)
where TTarget : notnull
{
if (value == null)
{
setter((TTarget)target, default!);
}
else
{
setter((TTarget)target, (TValue)value);
}
}
} }
} }

View File

@ -44,46 +44,5 @@ namespace Microsoft.AspNetCore.Components.Reflection
return dictionary.Values.SelectMany(p => p); return dictionary.Values.SelectMany(p => p);
} }
public static IPropertySetter CreatePropertySetter(Type targetType, PropertyInfo property, bool cascading)
{
if (property.SetMethod == null)
{
throw new InvalidOperationException($"Cannot provide a value for property " +
$"'{property.Name}' on type '{targetType.FullName}' because the property " +
$"has no setter.");
}
return (IPropertySetter)Activator.CreateInstance(
typeof(PropertySetter<,>).MakeGenericType(targetType, property.PropertyType),
property.SetMethod,
cascading)!;
}
class PropertySetter<TTarget, TValue> : IPropertySetter where TTarget : notnull
{
private readonly Action<TTarget, TValue> _setterDelegate;
public PropertySetter(MethodInfo setMethod, bool cascading)
{
_setterDelegate = (Action<TTarget, TValue>)Delegate.CreateDelegate(
typeof(Action<TTarget, TValue>), setMethod);
Cascading = cascading;
}
public bool Cascading { get; }
public void SetValue(object target, object value)
{
if (value == null)
{
_setterDelegate((TTarget)target, default!);
}
else
{
_setterDelegate((TTarget)target, (TValue)value);
}
}
}
} }
} }

View File

@ -0,0 +1,11 @@
<Project>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
<PropertyGroup>
<!-- Makes our docker composition simpler by not redirecting build and publish output to the artifacts dir -->
<BaseIntermediateOutputPath />
<IntermediateOutputPath />
<BaseOutputPath />
<OutputPath />
</PropertyGroup>
</Project>

View File

@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using OpenQA.Selenium;
using DevHostServerProgram = Microsoft.AspNetCore.Components.WebAssembly.DevServer.Server.Program; using DevHostServerProgram = Microsoft.AspNetCore.Components.WebAssembly.DevServer.Server.Program;
namespace Wasm.Performance.Driver namespace Wasm.Performance.Driver
@ -81,7 +82,20 @@ namespace Wasm.Performance.Driver
{ {
BenchmarkResultTask = new TaskCompletionSource<BenchmarkResult>(); BenchmarkResultTask = new TaskCompletionSource<BenchmarkResult>();
using var runCancellationToken = new CancellationTokenSource(timeForEachRun); using var runCancellationToken = new CancellationTokenSource(timeForEachRun);
using var registration = runCancellationToken.Token.Register(() => BenchmarkResultTask.TrySetException(new TimeoutException($"Timed out after {timeForEachRun}"))); using var registration = runCancellationToken.Token.Register(() =>
{
string exceptionMessage = $"Timed out after {timeForEachRun}.";
try
{
var innerHtml = browser.FindElement(By.CssSelector(":first-child")).GetAttribute("innerHTML");
exceptionMessage += Environment.NewLine + "Browser state: " + Environment.NewLine + innerHtml;
}
catch
{
// Do nothing;
}
BenchmarkResultTask.TrySetException(new TimeoutException(exceptionMessage));
});
var results = await BenchmarkResultTask.Task; var results = await BenchmarkResultTask.Task;
@ -89,6 +103,11 @@ namespace Wasm.Performance.Driver
includeMetadata: firstRun, includeMetadata: firstRun,
isStressRun: isStressRun); isStressRun: isStressRun);
if (!isStressRun)
{
PrettyPrint(results);
}
firstRun = false; firstRun = false;
} while (isStressRun && !stressRunCancellation.IsCancellationRequested); } while (isStressRun && !stressRunCancellation.IsCancellationRequested);
@ -230,6 +249,17 @@ namespace Wasm.Performance.Driver
Console.WriteLine(builder); Console.WriteLine(builder);
} }
static void PrettyPrint(BenchmarkResult benchmarkResult)
{
Console.WriteLine();
Console.WriteLine("| Name | Description | Duration | NumExecutions | ");
Console.WriteLine("--------------------------");
foreach (var result in benchmarkResult.ScenarioResults)
{
Console.WriteLine($"| {result.Descriptor.Name} | {result.Name} | {result.Duration} | {result.NumExecutions} |");
}
}
static IHost StartTestApp() static IHost StartTestApp()
{ {
var args = new[] var args = new[]

View File

@ -16,12 +16,7 @@ namespace Wasm.Performance.Driver
const int SeleniumPort = 4444; const int SeleniumPort = 4444;
static bool RunHeadlessBrowser = true; static bool RunHeadlessBrowser = true;
static bool PoolForBrowserLogs = static bool PoolForBrowserLogs = true;
#if DEBUG
true;
#else
false;
#endif
private static async ValueTask<Uri> WaitForServerAsync(int port, CancellationToken cancellationToken) private static async ValueTask<Uri> WaitForServerAsync(int port, CancellationToken cancellationToken)
{ {

View File

@ -3,6 +3,11 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<IsTestAssetProject>true</IsTestAssetProject> <IsTestAssetProject>true</IsTestAssetProject>
<!--
Chrome in docker appears to run in to cache corruption issues when the cache is read multiple times over.
Clien caching isn't part of our performance measurement, so we'll skip it.
-->
<BlazorCacheBootResources>false</BlazorCacheBootResources>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>