Skip to content

Commit

Permalink
benchmarks: Improve benchmark for better precision and run time.
Browse files Browse the repository at this point in the history
  • Loading branch information
jeanplevesque committed Jan 9, 2024
1 parent 8bc6744 commit 2ef2e88
Show file tree
Hide file tree
Showing 8 changed files with 378 additions and 58 deletions.
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: 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
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
246 changes: 246 additions & 0 deletions src/DynamicMvvm.Benchmarks/IViewModel.Extensions.Benchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
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 IViewModel CreateVM_For_UnresolvedProps()
//{
// return new NeverInitiatedViewModel();
//}

//[Benchmark]
//public IViewModel CreateVM_For_ResolvedProps()
//{
// return new InitiatedViewModel();
//}

[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);
}
}
}
22 changes: 21 additions & 1 deletion src/DynamicMvvm.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,25 @@
using Chinook.DynamicMvvm;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Order;

BenchmarkRunner.Run<Benchmark>();
BenchmarkRunner.Run(new[]
{
typeof(ViewModelBaseBenchmark),
typeof(ViewModelExtensionsBenchmark),
},
ManualConfig
.Create(DefaultConfig.Instance)
.WithOptions(ConfigOptions.JoinSummary)
.WithOrderer(new DefaultOrderer(SummaryOrderPolicy.Declared, MethodOrderPolicy.Declared))
.HideColumns("Type", "Job", "InvocationCount", "UnrollFactor", "Error", "StdDev", "MaxIterationCount", "MaxWarmupIterationCount")
);

// The following section is to profile manually using Visual Studio's debugger.

//Console.ReadKey();

//var serviceProvider = new HostBuilder()
// .ConfigureServices(serviceCollection => serviceCollection
// .AddSingleton<IDynamicCommandBuilderFactory, DynamicCommandBuilderFactory>()
Expand All @@ -16,7 +30,13 @@
// .Build()
// .Services;

//var vm = new InitiatedViewModel();
//vm.Number = 1;

//var vm1 = new ViewModel("ViewModel", serviceProvider);
//var vm2 = new ViewModel("ViewModel", serviceProvider);
//var value = vm1.NumberResolved;
//value = vm1.Number;
//Console.WriteLine(value);

//Console.Read();
79 changes: 79 additions & 0 deletions src/DynamicMvvm.Benchmarks/TestViewModelBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Chinook.DynamicMvvm;
using Microsoft.Extensions.DependencyInjection;

namespace DynamicMvvm.Benchmarks
{
/// <summary>
/// This implementation of IViewModel is used for testing the extension methods of IViewModel.
/// It's not a valid implementation of IViewModel.
/// </summary>
public class TestViewModelBase : IViewModel
{
public TestViewModelBase(IServiceProvider? serviceProvider = null)
{
ServiceProvider = serviceProvider;
}

public string Name => "TestViewModelBase";

public virtual IEnumerable<KeyValuePair<string, IDisposable>> Disposables => Enumerable.Empty<KeyValuePair<string, IDisposable>>();

public IDispatcher? Dispatcher { get; set; }

public IServiceProvider? ServiceProvider { get; set; }

public bool IsDisposed { get; set; }

public bool HasErrors => false;

public event Action<IDispatcher>? DispatcherChanged;
public event PropertyChangedEventHandler? PropertyChanged;
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;

public virtual void AddDisposable(string key, IDisposable disposable)
{
}

public void ClearErrors(string? propertyName = null)
{
}

public void Dispose()
{
}

public IEnumerable GetErrors(string? propertyName)
{
return Enumerable.Empty<object>();
}

public void RaisePropertyChanged(string propertyName)
{
}

public void RemoveDisposable(string key)
{
}

public void SetErrors(IDictionary<string, IEnumerable<object>> errors)
{
}

public void SetErrors(string propertyName, IEnumerable<object> errors)
{
}

public virtual bool TryGetDisposable(string key, out IDisposable? disposable)
{
disposable = default;
return false;
}
}
}
Loading

0 comments on commit 2ef2e88

Please sign in to comment.