Added fast cloner and benchmarks
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.14.0"/>
|
||||
<ProjectReference Include="..\Libraries\Core\Core.csproj"/>
|
||||
<ProjectReference Include="..\Libraries\SptAssets\SptAssets.csproj"/>
|
||||
<ProjectReference Include="..\Libraries\SptCommon\SptCommon.csproj"/>
|
||||
<ProjectReference Include="..\Libraries\SptDependencyInjection\SptDependencyInjection.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Assets\**">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,50 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Benchmarks.Mock;
|
||||
using Core.Models.Spt.Templates;
|
||||
using Core.Utils;
|
||||
using Core.Utils.Cloners;
|
||||
|
||||
namespace Benchmarks;
|
||||
|
||||
[SimpleJob(warmupCount: 10, iterationCount: 10)]
|
||||
[MemoryDiagnoser]
|
||||
public class ClonerBenchmarks
|
||||
{
|
||||
private Templates? _templates;
|
||||
|
||||
private ICloner _jsonCloner;
|
||||
private ICloner _reflectionsCloner;
|
||||
private ICloner _fastCloner;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
var jsonUtil = new JsonUtil();
|
||||
var importer = new ImporterUtil(new MockLogger<ImporterUtil>(), new FileUtil(new MockLogger<FileUtil>()),
|
||||
jsonUtil);
|
||||
var loadTask = importer.LoadRecursiveAsync<Templates>("./Assets/database/templates/");
|
||||
loadTask.Wait();
|
||||
_templates = loadTask.Result;
|
||||
_jsonCloner = new JsonCloner(jsonUtil);
|
||||
_reflectionsCloner = new ReflectionsCloner(new MockLogger<ReflectionsCloner>());
|
||||
_fastCloner = new Core.Utils.Cloners.FastCloner();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void JsonCloner()
|
||||
{
|
||||
_jsonCloner.Clone(_templates);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void ReflectionsCloner()
|
||||
{
|
||||
_reflectionsCloner.Clone(_templates);
|
||||
}
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public void FastCloner()
|
||||
{
|
||||
_fastCloner.Clone(_templates);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using Core.Models.Logging;
|
||||
using Core.Models.Spt.Logging;
|
||||
using Core.Models.Utils;
|
||||
|
||||
namespace Benchmarks.Mock;
|
||||
|
||||
public class MockLogger<T> : ISptLogger<T>
|
||||
{
|
||||
public void LogWithColor(string data, LogTextColor? textColor = null, LogBackgroundColor? backgroundColor = null, Exception? ex = null)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Success(string data, Exception? ex = null)
|
||||
{
|
||||
Console.WriteLine(data);
|
||||
}
|
||||
|
||||
public void Error(string data, Exception? ex = null)
|
||||
{
|
||||
Console.WriteLine(data);
|
||||
}
|
||||
|
||||
public void Warning(string data, Exception? ex = null)
|
||||
{
|
||||
Console.WriteLine(data);
|
||||
}
|
||||
|
||||
public void Info(string data, Exception? ex = null)
|
||||
{
|
||||
Console.WriteLine(data);
|
||||
}
|
||||
|
||||
public void Debug(string data, Exception? ex = null)
|
||||
{
|
||||
Console.WriteLine(data);
|
||||
}
|
||||
|
||||
public void Critical(string data, Exception? ex = null)
|
||||
{
|
||||
Console.WriteLine(data);
|
||||
}
|
||||
|
||||
public void WriteToLogFile(string body, LogLevel level = LogLevel.Info)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool IsLogEnabled(LogLevel level)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public void LogWithColor(
|
||||
string data,
|
||||
Exception? ex = null,
|
||||
LogTextColor? textColor = null,
|
||||
LogBackgroundColor? backgroundColor = null
|
||||
)
|
||||
{
|
||||
Console.WriteLine(data);
|
||||
}
|
||||
|
||||
public void WriteToLogFile(object body)
|
||||
{
|
||||
Console.WriteLine(body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using BenchmarkDotNet.Running;
|
||||
|
||||
namespace Benchmarks;
|
||||
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var summary = BenchmarkRunner.Run<ClonerBenchmarks>();
|
||||
Console.WriteLine(summary);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.IO.Hashing" Version="9.0.1" />
|
||||
<PackageReference Include="FastCloner" Version="3.3.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using SptCommon.Annotations;
|
||||
|
||||
namespace Core.Utils.Cloners;
|
||||
|
||||
[Injectable]
|
||||
public class FastCloner : ICloner
|
||||
{
|
||||
public T? Clone<T>(T? obj)
|
||||
{
|
||||
return global::FastCloner.FastCloner.DeepClone(obj);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using SptCommon.Annotations;
|
||||
|
||||
namespace Core.Utils.Cloners;
|
||||
|
||||
[Injectable]
|
||||
/**
|
||||
* Disabled as FastCloner library is 15% faster and consumes less memory than Json serialization
|
||||
*/
|
||||
public class JsonCloner : ICloner
|
||||
{
|
||||
protected JsonUtil _jsonUtil;
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using Core.Models.Utils;
|
||||
using Core.Utils.Json;
|
||||
using LogLevel = Core.Models.Spt.Logging.LogLevel;
|
||||
|
||||
namespace Core.Utils.Cloners;
|
||||
|
||||
/**
|
||||
* Not in use at the moment
|
||||
*/
|
||||
public class ReflectionsCloner(ISptLogger<ReflectionsCloner> logger) : ICloner
|
||||
{
|
||||
private static Dictionary<Type, MemberInfo[]> MemberInfoCache = new();
|
||||
private static Dictionary<Type, MethodInfo> AddMethodInfoCache = new();
|
||||
|
||||
public T? Clone<T>(T? obj)
|
||||
{
|
||||
return (T?) Clone(obj, typeof(T)).Result;
|
||||
}
|
||||
|
||||
public async Task<object?> Clone(object? obj, Type objectType)
|
||||
{
|
||||
// if its null, primitive, enum or string, just return the object
|
||||
if (obj == null)
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
{
|
||||
objectType = Nullable.GetUnderlyingType(objectType);
|
||||
}
|
||||
|
||||
if (objectType.IsPrimitive || objectType.IsEnum || obj is string)
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(ListOrT<>))
|
||||
{
|
||||
return await HandleSpecialClones(obj, objectType);
|
||||
}
|
||||
|
||||
var result = Activator.CreateInstance(objectType);
|
||||
|
||||
if (obj is IList listToClone)
|
||||
{
|
||||
foreach (var toClone in listToClone)
|
||||
{
|
||||
var cloned = await Clone(toClone, toClone.GetType());
|
||||
(result as IList).Add(cloned);
|
||||
}
|
||||
}
|
||||
else if (obj is IDictionary dictionaryToClone)
|
||||
{
|
||||
foreach (DictionaryEntry entryToClone in dictionaryToClone)
|
||||
{
|
||||
var clonedKey = await Clone(entryToClone.Key, entryToClone.Key.GetType());
|
||||
var clonedValue = await Clone(entryToClone.Value, entryToClone.Value.GetType());
|
||||
(result as IDictionary).Add(clonedKey, clonedValue);
|
||||
}
|
||||
}
|
||||
else if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(HashSet<>))
|
||||
{
|
||||
if (!AddMethodInfoCache.TryGetValue(objectType, out var addMethodInfo))
|
||||
{
|
||||
addMethodInfo = objectType.GetMethod("Add", BindingFlags.Instance | BindingFlags.Public);
|
||||
while (!AddMethodInfoCache.TryAdd(objectType, addMethodInfo)) ;
|
||||
}
|
||||
|
||||
var toCloneEnumerable = (IEnumerable) obj;
|
||||
|
||||
foreach (var toClone in toCloneEnumerable)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cloned = await Clone(toClone, toClone.GetType());
|
||||
addMethodInfo.Invoke(result, [cloned]);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (objectType.IsClass)
|
||||
{
|
||||
if (!MemberInfoCache.TryGetValue(objectType, out var memberInfos))
|
||||
{
|
||||
memberInfos = objectType.GetMembers(BindingFlags.Public | BindingFlags.Instance);
|
||||
while (!MemberInfoCache.TryAdd(objectType, memberInfos)) ;
|
||||
}
|
||||
|
||||
foreach (var member in memberInfos)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (member)
|
||||
{
|
||||
case PropertyInfo propertyInfo:
|
||||
|
||||
var propertyValue = propertyInfo.GetValue(obj, null);
|
||||
var propertyCloned = await Clone(propertyValue, propertyInfo.PropertyType);
|
||||
propertyInfo.SetValue(result, propertyCloned, null);
|
||||
|
||||
|
||||
break;
|
||||
case FieldInfo fieldInfo:
|
||||
|
||||
var fieldValue = fieldInfo.GetValue(obj);
|
||||
var fieldCloned = await Clone(fieldValue, fieldInfo.FieldType);
|
||||
fieldInfo.SetValue(result, fieldCloned);
|
||||
|
||||
|
||||
break;
|
||||
case MemberInfo:
|
||||
break;
|
||||
default:
|
||||
if (logger.IsLogEnabled(LogLevel.Debug))
|
||||
{
|
||||
logger.Debug($"Unknown member type {member.Name} {member.MemberType}");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (logger.IsLogEnabled(LogLevel.Debug))
|
||||
logger.Debug($"Clone of type {objectType} is not supported");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ConcurrentDictionary<Type, PropertyInfo> _itemPropertyInfoCache = new();
|
||||
private static ConcurrentDictionary<Type, PropertyInfo> _listPropertyInfoCache = new();
|
||||
|
||||
private async Task<object?> HandleSpecialClones(object obj, Type objectType)
|
||||
{
|
||||
if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(ListOrT<>))
|
||||
{
|
||||
var clone = Activator.CreateInstance(objectType, true);
|
||||
var type = objectType.GetGenericArguments()[0];
|
||||
if (!_itemPropertyInfoCache.TryGetValue(type, out var item))
|
||||
{
|
||||
item = objectType.GetProperty("Item", BindingFlags.Public | BindingFlags.Instance);
|
||||
while (!_itemPropertyInfoCache.TryAdd(type, item)) ;
|
||||
}
|
||||
|
||||
if (!_listPropertyInfoCache.TryGetValue(type, out var list))
|
||||
{
|
||||
list = objectType.GetProperty("List", BindingFlags.Public | BindingFlags.Instance);
|
||||
while (!_listPropertyInfoCache.TryAdd(type, list)) ;
|
||||
}
|
||||
|
||||
item.GetSetMethod(true).Invoke(clone, [await Clone(item.GetValue(obj), item.PropertyType)]);
|
||||
list.GetSetMethod(true).Invoke(clone, [await Clone(list.GetValue(obj), list.PropertyType)]);
|
||||
return clone;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,26 @@ namespace Core.Utils.Json;
|
||||
|
||||
public class ListOrT<T>(List<T>? list, T? item)
|
||||
{
|
||||
// Do not remove, its used by the cloner
|
||||
// ReSharper disable once UnusedMember.Local
|
||||
private ListOrT() : this(null, default)
|
||||
{
|
||||
}
|
||||
|
||||
public List<T>? List
|
||||
{
|
||||
get;
|
||||
// Do not remove, its used by the cloner
|
||||
// ReSharper disable once AutoPropertyCanBeMadeGetOnly.Local
|
||||
private set;
|
||||
} = list;
|
||||
|
||||
public T? Item
|
||||
{
|
||||
get;
|
||||
// do not remove, its used by the cloner
|
||||
// ReSharper disable once AutoPropertyCanBeMadeGetOnly.Local
|
||||
private set;
|
||||
} = item;
|
||||
|
||||
public bool IsItem
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using Core.Models.Spt.Templates;
|
||||
using Core.Utils;
|
||||
using Core.Utils.Cloners;
|
||||
using UnitTests.Mock;
|
||||
|
||||
namespace UnitTests.Tests.Utils;
|
||||
|
||||
[TestClass]
|
||||
public class ClonerTest
|
||||
{
|
||||
private Templates? _templates;
|
||||
|
||||
private ICloner _jsonCloner;
|
||||
private ICloner _reflectionsCloner;
|
||||
private ICloner _fastCloner;
|
||||
|
||||
private JsonUtil _jsonUtil;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
_jsonUtil = new JsonUtil();
|
||||
var importer = new ImporterUtil(new MockLogger<ImporterUtil>(), new FileUtil(new MockLogger<FileUtil>()), _jsonUtil);
|
||||
var loadTask = importer.LoadRecursiveAsync<Templates>("./TestAssets/");
|
||||
loadTask.Wait();
|
||||
_templates = loadTask.Result;
|
||||
_jsonCloner = new JsonCloner(_jsonUtil);
|
||||
_reflectionsCloner = new ReflectionsCloner(new MockLogger<ReflectionsCloner>());
|
||||
_fastCloner = new Core.Utils.Cloners.FastCloner();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FastCloner()
|
||||
{
|
||||
var jsonObject = _jsonCloner.Clone(_templates);
|
||||
var reflectionObject = _reflectionsCloner.Clone(_templates);
|
||||
var fastObject = _fastCloner.Clone(_templates);
|
||||
// This test is just used for cloner comparison, not a real test
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FastCloner" Version="3.3.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1"/>
|
||||
<PackageReference Include="MSTest" Version="3.6.1"/>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SptAssets", "Libraries\SptA
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HideoutCraftQuestIdGenerator", "Tools\HideoutCraftQuestIdGenerator\HideoutCraftQuestIdGenerator.csproj", "{C24B1FEB-F8AC-434E-998D-5DA4D1687295}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{6884273A-72E9-4035-B5BE-EE101C69F5F5}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -61,6 +63,10 @@ Global
|
||||
{C24B1FEB-F8AC-434E-998D-5DA4D1687295}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C24B1FEB-F8AC-434E-998D-5DA4D1687295}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C24B1FEB-F8AC-434E-998D-5DA4D1687295}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6884273A-72E9-4035-B5BE-EE101C69F5F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6884273A-72E9-4035-B5BE-EE101C69F5F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6884273A-72E9-4035-B5BE-EE101C69F5F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6884273A-72E9-4035-B5BE-EE101C69F5F5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Reference in New Issue
Block a user