Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

benchmarks: Improve benchmark for better precision and run time. #97

Merged
merged 1 commit into from
Jan 10, 2024
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: 2 additions & 0 deletions .github/.commitsar.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
commits:
strict: false
1 change: 1 addition & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ GitHub Issue: #
- [ ] **None** (The library is unchanged.)
- Only code under the `build` folder was changed.
- Only code under the `.github` folder was changed.
- Only code in the Benchmarks project was changed.

## Checklist

Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/conventional-commits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ jobs:
uses: actions/checkout@v1
- name: Commitsar check
uses: docker://aevea/commitsar
env:
COMMITSAR_CONFIG_PATH : ./.github
2 changes: 1 addition & 1 deletion build/gitversion.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ commit-message-incrementing: Enabled
major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)"
minor-version-bump-message: "^(feat)(\\([\\w\\s-]*\\))?:"
patch-version-bump-message: "^(build|chore|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?:"
no-bump-message: "^(ci)(\\([\\w\\s-]*\\))?:" # You can use the "ci" type to avoid bumping the version when your changes are limited to the build or .github folders.
no-bump-message: "^(ci|benchmarks)(\\([\\w\\s-]*\\))?:" # You can use the "ci" or "benchmarks" type to avoid bumping the version when your changes are limited to the [build or .github folders] or limited to benchmark code.
branches:
main:
regex: ^master$|^main$
Expand Down
111 changes: 0 additions & 111 deletions src/DynamicMvvm.Benchmarks/Benchmark.cs

This file was deleted.

1 change: 1 addition & 0 deletions src/DynamicMvvm.Benchmarks/DynamicMvvm.Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.11" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
<PackageReference Include="System.Reactive" Version="6.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
234 changes: 234 additions & 0 deletions src/DynamicMvvm.Benchmarks/IViewModel.Extensions.Benchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Reflection.Emit;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Chinook.DynamicMvvm;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace DynamicMvvm.Benchmarks
{
[MemoryDiagnoser]
[MaxIterationCount(36)]
[MaxWarmupCount(16)]
public class ViewModelExtensionsBenchmark
{
internal static readonly IServiceProvider ServiceProvider = new HostBuilder()
.ConfigureServices(serviceCollection => serviceCollection
.AddSingleton<IDynamicCommandBuilderFactory, DynamicCommandBuilderFactory>()
.AddSingleton<IDynamicPropertyFactory, DynamicPropertyFactory>()
)
.Build()
.Services;

internal static readonly Type DynamicViewModelType = GetDynamicViewModelType();
private const int PropertyCount = 32;
private const int ViewModelCount = 1024 * 1024;
private static string[] propertyNames = Enumerable.Range(0, PropertyCount).Select(i => $"Number{i}").ToArray();

/// <summary>
/// Generates a dynamic type that inherits from <see cref="ViewModelBase"/> and has <paramref name="propertyCount"/> properties.
/// </summary>
/// <param name="propertyCount">The amount of property defined in the dynamic class.</param>
/// <returns>A dynamically generated type.</returns>
internal static Type GetDynamicViewModelType(int propertyCount = PropertyCount)
{
// Create a new dynamic assembly
AssemblyName assemblyName = new AssemblyName("DynamicAssembly");
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);

// Create a new module within the assembly
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicModule");

// Create a new type that inherits from ViewModelBase
TypeBuilder typeBuilder = moduleBuilder.DefineType("DynamicClass", TypeAttributes.Public | TypeAttributes.Class, typeof(ViewModelBase));

// Define a parameterless constructor
ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { typeof(string), typeof(IServiceProvider) });
ILGenerator constructorIL = constructorBuilder.GetILGenerator();
constructorIL.Emit(OpCodes.Ldarg_0);
constructorIL.Emit(OpCodes.Ldarg_1);
constructorIL.Emit(OpCodes.Ldarg_2);
constructorIL.Emit(OpCodes.Call, typeof(ViewModelBase).GetConstructor(new Type[] { typeof(string), typeof(IServiceProvider) })!);
constructorIL.Emit(OpCodes.Ret);

// There is a name conflict on IViewModelExtensions which requires us to use reflection to get it (because typeof(IViewModelExtensions) is ambiguous).
var viewModelExtensionsType = Assembly.GetAssembly(typeof(IViewModel))!.GetType("Chinook.DynamicMvvm.IViewModelExtensions")!;

for (int i = 0; i < propertyCount; i++)
{
// Define a 'NumberX' property with the specified getter and setter
PropertyBuilder propertyBuilder = typeBuilder.DefineProperty("Number" + i, PropertyAttributes.None, typeof(int), null);

MethodBuilder getMethod = typeBuilder.DefineMethod("get_Number" + i, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, typeof(int), Type.EmptyTypes);
ILGenerator getMethodIL = getMethod.GetILGenerator();
getMethodIL.Emit(OpCodes.Ldarg_0);
getMethodIL.Emit(OpCodes.Ldc_I4_S, 42); // Initial value
getMethodIL.Emit(OpCodes.Ldstr, "Number" + i);
var getMethodInfo = viewModelExtensionsType.GetMethods()
.Where(m => m.Name == "Get" && m.IsGenericMethod)
.First(m =>
{
var parameters = m.GetParameters();
return parameters.Length == 3 && parameters[0].ParameterType == typeof(IViewModel) && parameters[2].ParameterType == typeof(string);
});
getMethodIL.EmitCall(OpCodes.Call, getMethodInfo.MakeGenericMethod(typeof(int)), null);
getMethodIL.Emit(OpCodes.Ret);

MethodBuilder setMethod = typeBuilder.DefineMethod("set_Number" + i, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, null, new Type[] { typeof(int) });
ILGenerator setMethodIL = setMethod.GetILGenerator();
setMethodIL.Emit(OpCodes.Ldarg_0);
setMethodIL.Emit(OpCodes.Ldarg_1);
setMethodIL.Emit(OpCodes.Ldstr, "Number" + i);
var setMethodInfo = viewModelExtensionsType.GetMethods()
.Where(m => m.Name == "Set" && m.IsGenericMethod)
.First(m =>
{
var parameters = m.GetParameters();
return parameters.Length == 3 && parameters[0].ParameterType == typeof(IViewModel) && parameters[2].ParameterType == typeof(string);
});
setMethodIL.EmitCall(OpCodes.Call, setMethodInfo.MakeGenericMethod(typeof(int)), null);
setMethodIL.Emit(OpCodes.Ret);

propertyBuilder.SetGetMethod(getMethod);
propertyBuilder.SetSetMethod(setMethod);
}

// Create the type
Type dynamicType = typeBuilder.CreateType();

return dynamicType;
}

/// <summary>
/// This vm always initializes new property instances when being invoked.
/// </summary>
private NeverInitiatedViewModel _neverInitiatedVM = new();

private InitiatedViewModel _initiatedVM = new();

private IViewModel[]? _vmsForPropertySetter;
private int _i = 0;

[GlobalSetup(Target = nameof(Set_Unresolved))]
public void SetupVMForPropertySetter()
{
_i = 0;
_vmsForPropertySetter = new IViewModel[ViewModelCount];
for (int i = 0; i < ViewModelCount; i++)
{
_vmsForPropertySetter[i] = (IViewModel)Activator.CreateInstance(DynamicViewModelType, "ViewModelName", ServiceProvider)!;
}
}

[Benchmark]
public int GetFromValue_Unresolved()
{
return _neverInitiatedVM.Number;
}

[Benchmark]
public int GetFromObservable_Unresolved()
{
return _neverInitiatedVM.ObservableNumber;
}

[Benchmark]
public int GetFromValue_Resolved()
{
return _initiatedVM.Number;
}

[Benchmark]
public int GetFromObservable_Resolved()
{
return _initiatedVM.ObservableNumber;
}

[Benchmark(OperationsPerInvoke = PropertyCount)]
[MaxIterationCount(24)]
public void Set_Unresolved()
{
var i = Interlocked.Increment(ref _i);
var vm = _vmsForPropertySetter![i];
for (int propertyIndex = 0; propertyIndex < PropertyCount; propertyIndex++)
{
vm!.Set(i, propertyNames[propertyIndex]);
}
}

[Benchmark]
public void Set_Resolved()
{
_initiatedVM.Number = 1;
}
}

public sealed class NeverInitiatedViewModel : TestViewModelBase
{
public NeverInitiatedViewModel(IServiceProvider? serviceProvider = null)
: base(serviceProvider ?? ViewModelExtensionsBenchmark.ServiceProvider)
{
}

public int Number
{
get => this.Get<int>(initialValue: 42);
set => this.Set(value);
}

public int ObservableNumber
{
get => this.GetFromObservable<int>(Observable.Never<int>(), initialValue: 0);
set => this.Set(value);
}
}

public sealed class InitiatedViewModel : TestViewModelWithProperty
{
public InitiatedViewModel()
{
ServiceProvider = ViewModelExtensionsBenchmark.ServiceProvider;

Resolve(Number);
Resolve(ObservableNumber);
}

public int Number
{
get => this.Get<int>(initialValue: 42);
set => this.Set(value);
}

public int ObservableNumber
{
get => this.GetFromObservable<int>(Observable.Never<int>(), initialValue: 0);
set => this.Set(value);
}
}

public class TestViewModelWithProperty : TestViewModelBase
{
IDictionary<string, IDisposable> _disposables = new Dictionary<string, IDisposable>();

protected void Resolve(object value)
{
}

public override void AddDisposable(string key, IDisposable disposable)
{
_disposables[key] = disposable;
}

public override bool TryGetDisposable(string key, out IDisposable? disposable)
{
return _disposables.TryGetValue(key, out disposable);
}
}
}
Loading