Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Components/Components/src/ComponentFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ private Action<IServiceProvider, IComponent> CreateInitializer(Type type)
(
propertyName: property.Name,
propertyType: property.PropertyType,
setter: MemberAssignment.CreatePropertySetter(type, property, cascading: false)
setter: new PropertySetter(type, property)
)).ToArray();

return Initialize;
Expand Down
21 changes: 12 additions & 9 deletions src/Components/Components/src/Reflection/ComponentProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ public static void SetProperties(in ParameterView parameters, object target)
}
}

static void SetProperty(object target, IPropertySetter writer, string parameterName, object value)
static void SetProperty(object target, PropertySetter writer, string parameterName, object value)
{
try
{
Expand Down Expand Up @@ -246,13 +246,13 @@ private static void ThrowForInvalidCaptureUnmatchedValuesParameterType(Type targ
private class WritersForType
{
private const int MaxCachedWriterLookups = 100;
private readonly Dictionary<string, IPropertySetter> _underlyingWriters;
private readonly ConcurrentDictionary<string, IPropertySetter?> _referenceEqualityWritersCache;
private readonly Dictionary<string, PropertySetter> _underlyingWriters;
private readonly ConcurrentDictionary<string, PropertySetter?> _referenceEqualityWritersCache;

public WritersForType(Type targetType)
{
_underlyingWriters = new Dictionary<string, IPropertySetter>(StringComparer.OrdinalIgnoreCase);
_referenceEqualityWritersCache = new ConcurrentDictionary<string, IPropertySetter?>(ReferenceEqualityComparer.Instance);
_underlyingWriters = new Dictionary<string, PropertySetter>(StringComparer.OrdinalIgnoreCase);
_referenceEqualityWritersCache = new ConcurrentDictionary<string, PropertySetter?>(ReferenceEqualityComparer.Instance);

foreach (var propertyInfo in GetCandidateBindableProperties(targetType))
{
Expand All @@ -271,7 +271,10 @@ public WritersForType(Type targetType)
$"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))
{
Expand All @@ -298,17 +301,17 @@ public WritersForType(Type targetType)
ThrowForInvalidCaptureUnmatchedValuesParameterType(targetType, propertyInfo);
}

CaptureUnmatchedValuesWriter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo, cascading: false);
CaptureUnmatchedValuesWriter = new PropertySetter(targetType, propertyInfo);
CaptureUnmatchedValuesPropertyName = propertyInfo.Name;
}
}
}

public IPropertySetter? CaptureUnmatchedValuesWriter { get; }
public PropertySetter? CaptureUnmatchedValuesWriter { 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
// lookup from parameterName to writer. Pre-5.0 that was because of the string hashing.
Expand Down
49 changes: 46 additions & 3 deletions src/Components/Components/src/Reflection/IPropertySetter.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,55 @@
// 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.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)!;

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 =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is identical to what PropertyHelper does: https://github.com/dotnet/aspnetcore/blob/master/src/Shared/PropertyHelper/PropertyHelper.cs#L312-L321. I wasn't sure if we intentionally avoided using that type in here for some reason, so I copied it over. It avoids a Activator.CreateInstance call that we previously had per property which would be an improvement in my books

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

void SetValue(object target, object 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);
}
}
}
}
41 changes: 0 additions & 41 deletions src/Components/Components/src/Reflection/MemberAssignment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,46 +44,5 @@ public static IEnumerable<PropertyInfo> GetPropertiesIncludingInherited(

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);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenQA.Selenium;
using DevHostServerProgram = Microsoft.AspNetCore.Components.WebAssembly.DevServer.Server.Program;

namespace Wasm.Performance.Driver
Expand Down Expand Up @@ -81,14 +82,32 @@ public static async Task<int> Main(string[] args)
{
BenchmarkResultTask = new TaskCompletionSource<BenchmarkResult>();
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;

FormatAsBenchmarksOutput(results,
includeMetadata: firstRun,
isStressRun: isStressRun);

if (!isStressRun)
{
PrettyPrint(results);
}

firstRun = false;
} while (isStressRun && !stressRunCancellation.IsCancellationRequested);

Expand Down Expand Up @@ -230,6 +249,17 @@ private static void FormatAsBenchmarksOutput(BenchmarkResult benchmarkResult, bo
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()
{
var args = new[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,7 @@ class Selenium
const int SeleniumPort = 4444;
static bool RunHeadlessBrowser = true;

static bool PoolForBrowserLogs =
#if DEBUG
true;
#else
false;
#endif
static bool PoolForBrowserLogs = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup. We had it turned off because running the benchmarks would bury you logs from the GC. But that's all gone now and it actually helps to see the benchmark progress when you're running it in a container.


private static async ValueTask<Uri> WaitForServerAsync(int port, CancellationToken cancellationToken)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<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>

<ItemGroup>
Expand Down