From 31d6b2a9e6a23287b7cd6e768cde8ce25f55b800 Mon Sep 17 00:00:00 2001 From: "2248356998 qq.com" <2248356998@qq.com> Date: Fri, 26 Sep 2025 15:24:37 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0FastMapper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Common/FastMapper.cs | 169 ++++++++++++++++++ src/Directory.Build.props | 6 +- .../Extension/ChannelOptionsExtensions.cs | 3 +- .../Trans/ThingsGatewayBitConverter.cs | 4 +- .../Entity/Device.cs | 2 +- .../DeviceManage/DeviceThreadManage.cs | 6 + .../Benchmark/MapperBenchmark.cs | 163 +++++++++++++++++ .../Benchmark/S7Benchmark.cs | 2 - .../Program.cs | 14 +- 9 files changed, 356 insertions(+), 13 deletions(-) create mode 100644 src/Admin/ThingsGateway.NewLife.X/Common/FastMapper.cs create mode 100644 src/Plugin/ThingsGateway.Foundation.Benchmark/Benchmark/MapperBenchmark.cs diff --git a/src/Admin/ThingsGateway.NewLife.X/Common/FastMapper.cs b/src/Admin/ThingsGateway.NewLife.X/Common/FastMapper.cs new file mode 100644 index 000000000..9b4fe8543 --- /dev/null +++ b/src/Admin/ThingsGateway.NewLife.X/Common/FastMapper.cs @@ -0,0 +1,169 @@ +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; + +namespace ThingsGateway.NewLife; + +public class FastMapperOption +{ + public Dictionary MapperProperties { get; set; } = new(); + public HashSet IgnoreProperties { get; set; } = new(); +} + +public static class FastMapper +{ + // 泛型 + 非泛型共用缓存 + private static readonly ConcurrentDictionary<(Type Source, Type Target), Delegate> _mapCache + = new ConcurrentDictionary<(Type, Type), Delegate>(); + + #region 泛型入口 + public static TTarget Mapper(TSource source, FastMapperOption option = null) + where TTarget : class, new() + { + if (source == null) return null; + + var key = (typeof(TSource), typeof(TTarget)); + if (!_mapCache.TryGetValue(key, out var del)) + { + del = CreateMapFunc(); + _mapCache[key] = del; + } + + var func = (Func)del; + return func(source, option); + } + #endregion + + #region 非泛型入口 + public static object Mapper(object source, Type targetType, FastMapperOption option = null) + { + if (source == null) return null; + + var sourceType = source.GetType(); + var key = (sourceType, targetType); + + if (!_mapCache.TryGetValue(key, out var del)) + { + // 动态生成泛型委托并缓存 + var method = typeof(FastMapper).GetMethod(nameof(CreateMapFunc), BindingFlags.NonPublic | BindingFlags.Static); + var genericMethod = method.MakeGenericMethod(sourceType, targetType); + del = genericMethod.Invoke(null, null) as Delegate; + _mapCache[key] = del; + } + + return del.DynamicInvoke(source, option); + } + #endregion + + #region Expression Tree 创建委托 + private static Func CreateMapFunc() + where TTarget : class, new() + { + var sourceType = typeof(TSource); + var targetType = typeof(TTarget); + + var sourceParam = Expression.Parameter(sourceType, "src"); + var optionParam = Expression.Parameter(typeof(FastMapperOption), "opt"); + var targetVar = Expression.Variable(targetType, "dest"); + + var expressions = new List + { + Expression.Assign(targetVar, Expression.New(targetType)) + }; + + var sourceProperties = sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance); + var targetProperties = targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance); + var targetDict = targetProperties.Where(p => p.CanWrite).ToDictionary(p => p.Name); + + foreach (var sp in sourceProperties) + { + if (!sp.CanRead) continue; + + // FastMapperOption 重命名 + var pNameExpr = Expression.Constant(sp.Name); + if (targetDict.TryGetValue(sp.Name, out PropertyInfo? tp)) + { + // Ignore check + Expression ignoreCheck = Expression.Call( + Expression.Property(optionParam, nameof(FastMapperOption.IgnoreProperties)), + nameof(HashSet.Contains), + null, + pNameExpr + ); + Expression assign; + + // 1️⃣ 简单类型直接赋值 + if (IsSimpleType(sp.PropertyType)) + { + assign = Expression.Assign( + Expression.Property(targetVar, tp), + Expression.Convert(Expression.Property(sourceParam, sp), tp.PropertyType) + ); + } + // 2️⃣ 集合类型 + else if (typeof(System.Collections.IEnumerable).IsAssignableFrom(sp.PropertyType) && sp.PropertyType != typeof(string)) + { + var elementSourceType = sp.PropertyType.IsArray + ? sp.PropertyType.GetElementType() + : sp.PropertyType.GetGenericArguments().FirstOrDefault(); + + var elementTargetType = tp.PropertyType.IsArray + ? tp.PropertyType.GetElementType() + : tp.PropertyType.GetGenericArguments().FirstOrDefault(); + + if (elementSourceType != null && elementTargetType != null) + { + var mapListMethod = typeof(FastMapper).GetMethod(nameof(MapList), BindingFlags.Public | BindingFlags.Static) + .MakeGenericMethod(elementSourceType, elementTargetType); + + assign = Expression.Assign( + Expression.Property(targetVar, tp), + Expression.Call(mapListMethod, Expression.Property(sourceParam, sp), optionParam) + ); + } + else continue; + } + // 3️⃣ 引用类型/嵌套对象 + else + { + var mapMethod = typeof(FastMapper).GetMethod(nameof(Mapper), new Type[] { typeof(object), typeof(Type), typeof(FastMapperOption) }); + var arg0 = Expression.Convert(Expression.Property(sourceParam, sp), typeof(object)); // ✅ fix Nullable/ValueType + assign = Expression.Assign( + Expression.Property(targetVar, tp), + Expression.Convert( + Expression.Call(mapMethod, arg0, Expression.Constant(tp.PropertyType), optionParam), + tp.PropertyType + ) + ); + } + + expressions.Add(assign); + } + } + + expressions.Add(targetVar); + var body = Expression.Block(new[] { targetVar }, expressions); + return Expression.Lambda>(body, sourceParam, optionParam).Compile(); + } + #endregion + + #region 泛型集合映射 + public static IEnumerable MapList(IEnumerable list, FastMapperOption option = null) + where TTarget : class, new() + { + if (list == null) yield break; + foreach (var item in list) + yield return Mapper(item, option); + } + #endregion + + private static bool IsSimpleType(Type type) + { + return type.IsPrimitive + || type.IsEnum + || type == typeof(string) + || type == typeof(decimal) + || type == typeof(DateTime) + || (Nullable.GetUnderlyingType(type) != null && IsSimpleType(Nullable.GetUnderlyingType(type))); + } +} diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 92a35a8fd..23edeacd6 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,9 +1,9 @@ - 10.11.71 - 10.11.71 - 10.11.71 + 10.11.72 + 10.11.72 + 10.11.72 10.11.6 10.11.6 8.0.20 diff --git a/src/Foundation/ThingsGateway.Foundation/Channel/Extension/ChannelOptionsExtensions.cs b/src/Foundation/ThingsGateway.Foundation/Channel/Extension/ChannelOptionsExtensions.cs index 6347e94bc..958ead510 100644 --- a/src/Foundation/ThingsGateway.Foundation/Channel/Extension/ChannelOptionsExtensions.cs +++ b/src/Foundation/ThingsGateway.Foundation/Channel/Extension/ChannelOptionsExtensions.cs @@ -9,6 +9,7 @@ //------------------------------------------------------------------------------ using ThingsGateway.Foundation.Extension.String; +using ThingsGateway.NewLife; using TouchSocket.SerialPorts; @@ -140,7 +141,7 @@ public static class ChannelOptionsExtensions /// private static SerialPortChannel GetSerialPort(this TouchSocketConfig config, IChannelOptions channelOptions) { - var serialPortOption = channelOptions.Map(); + var serialPortOption = FastMapper.Mapper(channelOptions); serialPortOption.ThrowIfNull(nameof(SerialPortOption)); channelOptions.Config = config; config.SetSerialPortOption(serialPortOption); diff --git a/src/Foundation/ThingsGateway.Foundation/Trans/ThingsGatewayBitConverter.cs b/src/Foundation/ThingsGateway.Foundation/Trans/ThingsGatewayBitConverter.cs index ecea50fc9..7e12e2bc6 100644 --- a/src/Foundation/ThingsGateway.Foundation/Trans/ThingsGatewayBitConverter.cs +++ b/src/Foundation/ThingsGateway.Foundation/Trans/ThingsGatewayBitConverter.cs @@ -15,6 +15,7 @@ using System.Text; using ThingsGateway.Foundation.Extension.Generic; using ThingsGateway.Foundation.Extension.String; +using ThingsGateway.NewLife; using TouchSocket.Resources; @@ -187,8 +188,7 @@ public partial class ThingsGatewayBitConverter : IThingsGatewayBitConverter // 更新设备地址为去除附加信息后的地址 registerAddress = sb.ToString(); - - var converter = (IThingsGatewayBitConverter)this!.Map(type); + var converter = (IThingsGatewayBitConverter)FastMapper.Mapper(this, type); // 如果没有解析出任何附加信息,则直接返回默认的数据转换器 if (bcdFormat == null && stringlength == null && encoding == null && dataFormat == null && wstring == null) { diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Entity/Device.cs b/src/Gateway/ThingsGateway.Gateway.Application/Entity/Device.cs index fecf24689..811e1398f 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Entity/Device.cs +++ b/src/Gateway/ThingsGateway.Gateway.Application/Entity/Device.cs @@ -121,7 +121,7 @@ public class Device : BaseDataEntity, IValidatableObject /// [SugarColumn(ColumnDescription = "冗余扫描间隔")] [AutoGenerateColumn(Visible = true, Filterable = true, Sortable = true)] - [MinValue(30000)] + [MinValue(10000)] public virtual int RedundantScanIntervalTime { get; set; } = 30000; /// diff --git a/src/Gateway/ThingsGateway.Gateway.Application/Services/GatewayMonitor/DeviceManage/DeviceThreadManage.cs b/src/Gateway/ThingsGateway.Gateway.Application/Services/GatewayMonitor/DeviceManage/DeviceThreadManage.cs index 73340e2ab..d2c33924c 100644 --- a/src/Gateway/ThingsGateway.Gateway.Application/Services/GatewayMonitor/DeviceManage/DeviceThreadManage.cs +++ b/src/Gateway/ThingsGateway.Gateway.Application/Services/GatewayMonitor/DeviceManage/DeviceThreadManage.cs @@ -330,6 +330,12 @@ internal sealed class DeviceThreadManage : IAsyncDisposable, IDeviceThreadManage driver.IsInitSuccess = false; LogMessage?.LogWarning(ex, string.Format(AppResource.InitFail, CurrentChannel.PluginName, driver?.DeviceName)); } + if (driver == null) + { + LogMessage?.LogWarning(string.Format(AppResource.InitFail, CurrentChannel.PluginName, driver?.DeviceName)); + return; + } + if (driver?.DeviceId > 0) { if (CancellationTokenSources.TryGetValue(driver.DeviceId, out var oldCts)) diff --git a/src/Plugin/ThingsGateway.Foundation.Benchmark/Benchmark/MapperBenchmark.cs b/src/Plugin/ThingsGateway.Foundation.Benchmark/Benchmark/MapperBenchmark.cs new file mode 100644 index 000000000..cf89ee68b --- /dev/null +++ b/src/Plugin/ThingsGateway.Foundation.Benchmark/Benchmark/MapperBenchmark.cs @@ -0,0 +1,163 @@ +//------------------------------------------------------------------------------ +// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充 +// 此代码版权(除特别声明外的代码)归作者本人Diego所有 +// 源代码使用协议遵循本仓库的开源协议及附加协议 +// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway +// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway +// 使用文档:https://kimdiego2098.github.io/ +// QQ群:605534569 +//------------------------------------------------------------------------------ + +using BenchmarkDotNet.Diagnosers; + +namespace ThingsGateway.Foundation; + +using BenchmarkDotNet.Attributes; + +using System; +using System.Collections.Generic; + +using ThingsGateway.NewLife; + +using TouchSocket.Core; + +public class Foo +{ + public string Name { get; set; } + public int Age { get; set; } + public int Age1 { get; set; } + public int Age2 { get; set; } + public int Age3 { get; set; } + public int Age11 { get; set; } + public int Age21 { get; set; } + public int Age31 { get; set; } + +} + +public class Bar +{ + public string Name { get; set; } + public int Age { get; set; } + public Bar Child { get; set; } + public int Age1 { get; set; } + public int Age2 { get; set; } + public int Age3 { get; set; } + public int Age31 { get; set; } + public int Age32 { get; set; } + public int Age33 { get; set; } +} + +[RankColumn] +[MemoryDiagnoser] +public class MapperBench +{ + private List foos; + + [GlobalSetup] + public void Setup() + { + const int N = 100_000; + foos = new List(N); + for (int i = 0; i < N; i++) + { + foos.Add(new Foo + { + Name = $"Name{i}", + Age = i, + }); + } + } + + [Benchmark(Baseline = true)] + public List ManualConstructionMap100_000() + { + var bars = new List(foos.Count); + foreach (var f in foos) + { + bars.Add(new Bar + { + Name = f.Name, + Age = f.Age, + }); + } + return bars; + } + + [Benchmark] + public List ReflectionMap100_000() + { + var bars = new List(foos.Count); + foreach (var f in foos) + { + bars.Add(OriginalMapper(f)); + } + return bars; + } + + [Benchmark] + public List FastMapper100_000() + { + var bars = new List(foos.Count); + foreach (var f in foos) + { + bars.Add(FastMapper.Mapper(f)); + } + return bars; + } + + [Benchmark] + public List ReflectionCachedMap100_000() + { + var bars = new List(foos.Count); + foreach (var f in foos) + { + bars.Add(f.Map()); + } + return bars; + } + + + private static Bar OriginalMapper(Foo source) + { + if (source == null) return null; + + var target = new Bar(); + var st = typeof(Foo).GetProperties(); + var tt = typeof(Bar).GetProperties(); + + foreach (var sp in st) + { + var tp = Array.Find(tt, x => x.Name == sp.Name); + if (tp == null || !tp.CanWrite) continue; + + var value = sp.GetValue(source); + + if (value == null) + { + tp.SetValue(target, null); + } + else if (IsSimpleType(sp.PropertyType)) + { + // 基础类型直接赋值 + tp.SetValue(target, value); + } + else + { + // 嵌套对象递归映射 + tp.SetValue(target, value); + } + } + + return target; + } + + private static bool IsSimpleType(Type type) + { + return type.IsPrimitive || type.IsEnum || type == typeof(string) || type == typeof(decimal) || type == typeof(DateTime); + } + +} + + + + diff --git a/src/Plugin/ThingsGateway.Foundation.Benchmark/Benchmark/S7Benchmark.cs b/src/Plugin/ThingsGateway.Foundation.Benchmark/Benchmark/S7Benchmark.cs index b63857266..90676f769 100644 --- a/src/Plugin/ThingsGateway.Foundation.Benchmark/Benchmark/S7Benchmark.cs +++ b/src/Plugin/ThingsGateway.Foundation.Benchmark/Benchmark/S7Benchmark.cs @@ -27,8 +27,6 @@ namespace ThingsGateway.Foundation; [SimpleJob(RuntimeMoniker.Net80)] //[SimpleJob(RuntimeMoniker.Net10_0)] [MemoryDiagnoser] -[BaselineColumn] -[RankColumn] public class S7Benchmark : IDisposable { private List siemensS7s = new(); diff --git a/src/Plugin/ThingsGateway.Foundation.Benchmark/Program.cs b/src/Plugin/ThingsGateway.Foundation.Benchmark/Program.cs index 2268af7cd..7982a4808 100644 --- a/src/Plugin/ThingsGateway.Foundation.Benchmark/Program.cs +++ b/src/Plugin/ThingsGateway.Foundation.Benchmark/Program.cs @@ -45,10 +45,16 @@ namespace BenchmarkConsoleApp //ManualConfig.Create(DefaultConfig.Instance) //.WithOptions(ConfigOptions.DisableOptimizationsValidator) //); - BenchmarkRunner.Run( - ManualConfig.Create(DefaultConfig.Instance) - .WithOptions(ConfigOptions.DisableOptimizationsValidator) - ); + + BenchmarkRunner.Run( +ManualConfig.Create(DefaultConfig.Instance) +.WithOptions(ConfigOptions.DisableOptimizationsValidator) +); + + // BenchmarkRunner.Run( + // ManualConfig.Create(DefaultConfig.Instance) + // .WithOptions(ConfigOptions.DisableOptimizationsValidator) + //); // BenchmarkRunner.Run( //ManualConfig.Create(DefaultConfig.Instance) //.WithOptions(ConfigOptions.DisableOptimizationsValidator)