diff --git a/AyCode.Core.Serializers.Console/AyCode.Core.Serializers.Console.csproj b/AyCode.Core.Serializers.Console/AyCode.Core.Serializers.Console.csproj new file mode 100644 index 0000000..acdcc68 --- /dev/null +++ b/AyCode.Core.Serializers.Console/AyCode.Core.Serializers.Console.csproj @@ -0,0 +1,19 @@ + + + + + + + + + + + + + Exe + net9.0 + enable + enable + + + diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs new file mode 100644 index 0000000..ca5abb4 --- /dev/null +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -0,0 +1,195 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Tests.TestModels; +using MessagePack; +using MessagePack.Resolvers; + +namespace AyCode.Core.Serializers.Console; + +/// +/// Console application for Performance Diagnostics profiling. +/// Run with: Debug > Performance Profiler in Visual Studio +/// +/// Usage: +/// dotnet run -- serialize # Profile serialize only +/// dotnet run -- deserialize # Profile deserialize only +/// dotnet run -- all # Profile both (default) +/// +public static class Program +{ + private const int WarmupIterations = 50; + private const int TestIterations = 5000; + + // Keep references to prevent GC during profiling + private static TestOrder s_testOrder = null!; + private static byte[] s_acBinaryData = null!; + private static byte[] s_acBinaryNoRefData = null!; + private static byte[] s_msgPackData = null!; + private static AcBinarySerializerOptions s_acBinaryOptions = null!; + private static AcBinarySerializerOptions s_acBinaryNoRefOptions = null!; + private static MessagePackSerializerOptions s_msgPackOptions = null!; + + public static void Main(string[] args) + { + var mode = args.Length > 0 ? args[0].ToLower() : "all"; + + System.Console.WriteLine("=".PadRight(60, '=')); + System.Console.WriteLine($"AcBinary Performance Profiler - Mode: {mode}"); + System.Console.WriteLine("=".PadRight(60, '=')); + + Setup(); + Warmup(); + + System.Console.WriteLine($"\nRunning {TestIterations} iterations...\n"); + + var sw = Stopwatch.StartNew(); + + switch (mode) + { + case "serialize": + case "ser": + RunSerializeTests(); + break; + case "deserialize": + case "des": + RunDeserializeTests(); + break; + default: + RunSerializeTests(); + RunDeserializeTests(); + break; + } + + sw.Stop(); + System.Console.WriteLine($"\nTotal time: {sw.ElapsedMilliseconds:N0} ms"); + System.Console.WriteLine("=".PadRight(60, '=')); + } + + private static void Setup() + { + System.Console.WriteLine("Creating test data..."); + TestDataFactory.ResetIdCounter(); + var sharedTag = TestDataFactory.CreateTag("SharedTag"); + var sharedUser = TestDataFactory.CreateUser("shareduser"); + var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true); + + s_testOrder = TestDataFactory.CreateOrder( + itemCount: 3, + palletsPerItem: 3, + measurementsPerPallet: 3, + pointsPerMeasurement: 4, + sharedTag: sharedTag, + sharedUser: sharedUser, + sharedMetadata: sharedMeta); + + s_acBinaryOptions = AcBinarySerializerOptions.Default; + s_acBinaryNoRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); + s_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); + + s_acBinaryData = AcBinarySerializer.Serialize(s_testOrder, s_acBinaryOptions); + s_acBinaryNoRefData = AcBinarySerializer.Serialize(s_testOrder, s_acBinaryNoRefOptions); + s_msgPackData = MessagePackSerializer.Serialize(s_testOrder, s_msgPackOptions); + + System.Console.WriteLine($" AcBinary (WithRef): {s_acBinaryData.Length:N0} bytes"); + System.Console.WriteLine($" AcBinary (NoRef): {s_acBinaryNoRefData.Length:N0} bytes"); + System.Console.WriteLine($" MessagePack: {s_msgPackData.Length:N0} bytes"); + } + + private static void Warmup() + { + System.Console.WriteLine($"Warming up ({WarmupIterations} iterations)..."); + for (int i = 0; i < WarmupIterations; i++) + { + DoSerializeAcBinary(); + DoSerializeAcBinaryNoRef(); + DoSerializeMsgPack(); + DoDeserializeAcBinary(); + DoDeserializeAcBinaryNoRef(); + DoDeserializeMsgPack(); + } + } + + private static void RunSerializeTests() + { + System.Console.WriteLine("--- SERIALIZE ---"); + + var sw = Stopwatch.StartNew(); + for (int i = 0; i < TestIterations; i++) + { + DoSerializeAcBinary(); + } + sw.Stop(); + System.Console.WriteLine($" AcBinary (WithRef): {sw.ElapsedMilliseconds,6:N0} ms"); + + sw.Restart(); + for (int i = 0; i < TestIterations; i++) + { + DoSerializeAcBinaryNoRef(); + } + sw.Stop(); + System.Console.WriteLine($" AcBinary (NoRef): {sw.ElapsedMilliseconds,6:N0} ms"); + + sw.Restart(); + for (int i = 0; i < TestIterations; i++) + { + DoSerializeMsgPack(); + } + sw.Stop(); + System.Console.WriteLine($" MessagePack: {sw.ElapsedMilliseconds,6:N0} ms"); + } + + private static void RunDeserializeTests() + { + System.Console.WriteLine("--- DESERIALIZE ---"); + + var sw = Stopwatch.StartNew(); + for (int i = 0; i < TestIterations; i++) + { + DoDeserializeAcBinary(); + } + sw.Stop(); + System.Console.WriteLine($" AcBinary (WithRef): {sw.ElapsedMilliseconds,6:N0} ms"); + + sw.Restart(); + for (int i = 0; i < TestIterations; i++) + { + DoDeserializeAcBinaryNoRef(); + } + sw.Stop(); + System.Console.WriteLine($" AcBinary (NoRef): {sw.ElapsedMilliseconds,6:N0} ms"); + + sw.Restart(); + for (int i = 0; i < TestIterations; i++) + { + DoDeserializeMsgPack(); + } + sw.Stop(); + System.Console.WriteLine($" MessagePack: {sw.ElapsedMilliseconds,6:N0} ms"); + } + + // Separate methods for better profiler visibility - NO INLINING + [MethodImpl(MethodImplOptions.NoInlining)] + private static byte[] DoSerializeAcBinary() + => AcBinarySerializer.Serialize(s_testOrder, s_acBinaryOptions); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static byte[] DoSerializeAcBinaryNoRef() + => AcBinarySerializer.Serialize(s_testOrder, s_acBinaryNoRefOptions); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static byte[] DoSerializeMsgPack() + => MessagePackSerializer.Serialize(s_testOrder, s_msgPackOptions); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static TestOrder? DoDeserializeAcBinary() + => AcBinaryDeserializer.Deserialize(s_acBinaryData); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static TestOrder? DoDeserializeAcBinaryNoRef() + => AcBinaryDeserializer.Deserialize(s_acBinaryNoRefData); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static TestOrder? DoDeserializeMsgPack() + => MessagePackSerializer.Deserialize(s_msgPackData, s_msgPackOptions); +} diff --git a/AyCode.Core.sln b/AyCode.Core.sln index 02490a2..54c8305 100644 --- a/AyCode.Core.sln +++ b/AyCode.Core.sln @@ -53,6 +53,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Services.Tests", "Ay EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Core.Serializers.SourceGenerator", "AyCode.Core.Serializers.SourceGenerator\AyCode.Core.Serializers.SourceGenerator.csproj", "{4A817897-80A8-4F42-86C5-20447401E0AA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Core.Serializers.Console", "AyCode.Core.Serializers.Console\AyCode.Core.Serializers.Console.csproj", "{6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -433,6 +435,24 @@ Global {4A817897-80A8-4F42-86C5-20447401E0AA}.Release|x64.Build.0 = Release|Any CPU {4A817897-80A8-4F42-86C5-20447401E0AA}.Release|x86.ActiveCfg = Release|Any CPU {4A817897-80A8-4F42-86C5-20447401E0AA}.Release|x86.Build.0 = Release|Any CPU + {6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Debug|x64.ActiveCfg = Debug|Any CPU + {6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Debug|x64.Build.0 = Debug|Any CPU + {6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Debug|x86.ActiveCfg = Debug|Any CPU + {6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Debug|x86.Build.0 = Debug|Any CPU + {6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Product|Any CPU.ActiveCfg = Release|Any CPU + {6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Product|Any CPU.Build.0 = Release|Any CPU + {6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Product|x64.ActiveCfg = Release|Any CPU + {6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Product|x64.Build.0 = Release|Any CPU + {6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Product|x86.ActiveCfg = Release|Any CPU + {6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Product|x86.Build.0 = Release|Any CPU + {6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Release|Any CPU.Build.0 = Release|Any CPU + {6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Release|x64.ActiveCfg = Release|Any CPU + {6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Release|x64.Build.0 = Release|Any CPU + {6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Release|x86.ActiveCfg = Release|Any CPU + {6AB7CE43-3C98-1D54-9ABD-E5E9364541E7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs index d7ca6dc..746c28c 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs @@ -151,7 +151,7 @@ public static partial class AcBinaryDeserializer public new bool IsIIdCollection => _isManualConstruction ? _manualIsIIdCollection : base.IsIIdCollection; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public new object? GetValue(object target) => _isManualConstruction ? _manualGetter!(target) : base.GetDynamicValue(target); + public new object? GetValue(object target) => _isManualConstruction ? _manualGetter!(target) : base.GetValue(target); public override void SetValue(object target, object? value) { diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs index 42bca2b..b134256 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs @@ -110,10 +110,10 @@ public static partial class AcBinaryDeserializer var existingObj = propInfo.GetValue(target); if (existingObj != null) { - var wrapper = context.ContextClass.GetWrapper(propInfo.PropertyType); - context.ReadByte(); // consume Object marker + var wrapper = context.ContextClass.GetWrapper(propInfo.PropertyType); + // Handle ref ID if present if (context.HasReferenceHandling) { @@ -135,7 +135,9 @@ public static partial class AcBinaryDeserializer try { // Use typed setters for primitives to avoid boxing - if (TryReadAndSetTypedValue(ref context, target, propInfo, peekCode)) + // Skip method call for Object/String/Collection types - they can't use typed setters + if (propInfo.AccessorType != PropertyAccessorType.Object && + TryReadAndSetTypedValue(ref context, target, propInfo, peekCode)) continue; var value = ReadValue(ref context, propInfo.PropertyType, nextDepth); @@ -214,9 +216,16 @@ public static partial class AcBinaryDeserializer try { - var wrapper = context.ContextClass.GetWrapper(elementType); var existingCount = existingList.Count; + // Early exit if empty source - just clear destination + if (count == 0) + { + existingList.Clear(); + return; + } + + var wrapper = context.ContextClass.GetWrapper(elementType); var elementMetadata = wrapper.Metadata.IsComplexType ? wrapper.Metadata : null; for (int i = 0; i < count; i++) diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 01d1291..f4c9c53 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -89,6 +89,7 @@ public static partial class AcBinarySerializer public bool UseMetadata { get; private set; } public byte MaxDepth { get; private set; } public byte MinStringInternLength { get; private set; } + public byte MaxStringInternLength { get; private set; } public BinaryPropertyFilter? PropertyFilter { get; private set; } public int Position => _position; @@ -114,6 +115,7 @@ public static partial class AcBinarySerializer UseMetadata = options.UseMetadata; MaxDepth = options.MaxDepth; MinStringInternLength = options.MinStringInternLength; + MaxStringInternLength = options.MaxStringInternLength; PropertyFilter = options.PropertyFilter; _initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize); diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index d299588..1440cd6 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -1,4 +1,4 @@ -using AyCode.Core.Helpers; +using AyCode.Core.Helpers; using AyCode.Core.Serializers.Expressions; using System.Buffers; using System.Collections; @@ -597,7 +597,11 @@ public static partial class AcBinarySerializer return; } - if (context.UseStringInterning && value.Length >= context.MinStringInternLength) + // String interning: only for strings within length range + // MaxStringInternLength == 0 means no max limit + if (context.UseStringInterning + && value.Length >= context.MinStringInternLength + && (context.MaxStringInternLength == 0 || value.Length <= context.MaxStringInternLength)) { var index = context.RegisterInternedString(value); context.WriteByte(BinaryTypeCode.StringInterned); @@ -642,12 +646,12 @@ public static partial class AcBinarySerializer case AcSerializerCommon.IdAccessorType.Int32: if (!context.TryTrack(wrapper, value, out int intId)) { - // Already seen → write reference + // Already seen › write reference context.WriteByte(BinaryTypeCode.ObjectRef); context.WriteVarInt(intId); return; } - // First occurrence → write object with refId + // First occurrence › write object with refId context.WriteByte(BinaryTypeCode.Object); context.WriteVarInt(intId); break; @@ -747,7 +751,7 @@ public static partial class AcBinarySerializer return false; default: // Object type - use regular getter - var value = prop.GetDynamicValue(obj); + var value = prop.GetValue(obj); if (value == null) return true; if (prop.PropertyTypeCode == TypeCode.String) return string.IsNullOrEmpty((string)value); return false; @@ -818,7 +822,7 @@ public static partial class AcBinarySerializer return; default: // Fallback to object getter for reference types - var value = prop.GetDynamicValue(obj); + var value = prop.GetValue(obj); WriteValue(value, prop.PropertyType, context, depth); return; } @@ -975,7 +979,7 @@ public static partial class AcBinarySerializer default: { // Object type - use regular getter - var value = prop.GetDynamicValue(obj); + var value = prop.GetValue(obj); // SKIP marker only for null (reference types) // Empty string, empty collections, etc. are valid values and must be written! @@ -1211,4 +1215,4 @@ public static partial class AcBinarySerializer // Type metadata helpers moved to AcBinarySerializer.BinarySerializeTypeMetadata.cs #endregion -} \ No newline at end of file +} diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index 0961010..038b33e 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -102,6 +102,14 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// public byte MinStringInternLength { get; init; } = 4; + /// + /// Maximum string length to consider for interning. + /// Longer strings (descriptions, notes, etc.) are usually unique and not worth interning. + /// Set to 0 to disable max limit. + /// Default: 64 (strings longer than 64 chars are not interned) + /// + public byte MaxStringInternLength { get; init; } = 64; + /// /// Initial capacity for serialization buffer. /// Default: 4096 bytes diff --git a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonElement.cs b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonElement.cs index 050aa2e..706fe3c 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonElement.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonElement.cs @@ -116,7 +116,7 @@ public static partial class AcJsonDeserializer for (var i = 0; i < props.Length; i++) { var prop = props[i]; - var value = prop.GetDynamicValue(source); + var value = prop.GetValue(source); if (value != null) prop.SetValue(target, value); } @@ -176,7 +176,7 @@ public static partial class AcJsonDeserializer // Handle IId collection merge if (propInfo.IsIIdCollection && propValueKind == JsonValueKind.Array) { - var existingCollection = propInfo.GetDynamicValue(target); + var existingCollection = propInfo.GetValue(target); if (existingCollection != null) { MergeIIdCollection(propValue, existingCollection, propInfo, context, depth); @@ -201,7 +201,7 @@ public static partial class AcJsonDeserializer // Merge into existing object if (!propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType)) { - var existingObj = propInfo.GetDynamicValue(target); + var existingObj = propInfo.GetValue(target); if (existingObj != null) { var nestedMetadata = GetTypeMetadata(propInfo.PropertyType); diff --git a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.Utf8Reader.cs b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.Utf8Reader.cs index dd4b407..d2a4657 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.Utf8Reader.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.Utf8Reader.cs @@ -489,7 +489,7 @@ public static partial class AcJsonDeserializer // Handle IId collection merge if (propInfo.IsIIdCollection && tokenType == JsonTokenType.StartArray) { - var existingCollection = propInfo.GetDynamicValue(target); + var existingCollection = propInfo.GetValue(target); if (existingCollection != null) { MergeIIdCollectionFromReader(ref reader, existingCollection, propInfo, maxDepth, depth); @@ -500,7 +500,7 @@ public static partial class AcJsonDeserializer // Handle nested objects - merge into existing if (tokenType == JsonTokenType.StartObject && !propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType)) { - var existingObj = propInfo.GetDynamicValue(target); + var existingObj = propInfo.GetValue(target); if (existingObj != null) { var nestedMetadata = GetTypeMetadata(propInfo.PropertyType); @@ -611,7 +611,7 @@ public static partial class AcJsonDeserializer { foreach (var prop in metadata.PropertySettersFrozen.Values) { - var value = prop.GetDynamicValue(source); + var value = prop.GetValue(source); if (value != null) prop.SetValue(target, value); } diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs index a16c277..02e1b4b 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs @@ -149,7 +149,7 @@ public static partial class AcJsonSerializer var propCount = props.Length; for (var i = 0; i < propCount; i++) { - var propValue = props[i].GetDynamicValue(value); + var propValue = props[i].GetValue(value); if (propValue != null) ScanReferences(propValue, context, depth + 1); } } @@ -205,7 +205,7 @@ public static partial class AcJsonSerializer for (var i = 0; i < propCount; i++) { var prop = props[i]; - var propValue = prop.GetDynamicValue(value); + var propValue = prop.GetValue(value); if (propValue == null) continue; if (IsDefaultValueFast(propValue, prop.PropertyTypeCode, prop.PropertyType)) continue; diff --git a/AyCode.Core/Serializers/PropertyAccessorBase.cs b/AyCode.Core/Serializers/PropertyAccessorBase.cs index 59cea81..77a4f85 100644 --- a/AyCode.Core/Serializers/PropertyAccessorBase.cs +++ b/AyCode.Core/Serializers/PropertyAccessorBase.cs @@ -1,90 +1,16 @@ using System.Reflection; using System.Runtime.CompilerServices; -using System.Text; using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers; /// -/// Enum for typed property accessor dispatch. -/// -public enum PropertyAccessorType : byte -{ - Object = 0, - Int32, - Int64, - Boolean, - Double, - Single, - Decimal, - DateTime, - Byte, - Int16, - UInt16, - UInt32, - UInt64, - Guid, - Enum -} - -/// -/// Base class for property accessors used by all serializers. -/// Contains common property metadata, getter functionality, and typed delegate fields. +/// Base class for property accessors used by serializers. +/// Contains getter functionality and typed delegate fields. /// Typed getters eliminate runtime cast overhead for value type properties. /// -public abstract class PropertyAccessorBase +public abstract class PropertyAccessorBase : PropertyMetadataBase { - /// - /// Property name. - /// - public string Name { get; } - - /// - /// Pre-encoded UTF8 bytes of property name for fast matching. - /// - public byte[] NameUtf8 { get; } - - /// - /// The property type (may be nullable). - /// - public Type PropertyType { get; } - - /// - /// The underlying type (unwrapped from Nullable if applicable). - /// - public Type UnderlyingType { get; } - - /// - /// Cached TypeCode for fast primitive type dispatch. - /// - public TypeCode PropertyTypeCode { get; } - - /// - /// Whether the property type is nullable. - /// - public bool IsNullable { get; } - - /// - /// The declaring type of this property. - /// - public Type DeclaringType { get; } - - /// - /// True if this property needs recursive scanning (not primitive/string). - /// Pre-computed to avoid IsPrimitiveOrStringFast() calls in hot path. - /// - public bool IsComplexType { get; } - - /// - /// The accessor type for fast typed getter dispatch. - /// - public PropertyAccessorType AccessorType { get; } - - /// - /// Compiled getter delegate for reading property values (boxed). - /// - protected readonly Func _dynamicGetter; - #region Strongly-typed getter delegate fields (eliminates runtime cast) // Only ONE of these is set based on AccessorType @@ -104,58 +30,12 @@ public abstract class PropertyAccessorBase #endregion - protected PropertyAccessorBase(PropertyInfo prop, Type declaringType) + protected PropertyAccessorBase(PropertyInfo prop, Type declaringType) + : base(prop, declaringType) { - Name = prop.Name; - NameUtf8 = Encoding.UTF8.GetBytes(prop.Name); - DeclaringType = declaringType; - PropertyType = prop.PropertyType; - - var underlying = Nullable.GetUnderlyingType(PropertyType); - IsNullable = underlying != null; - UnderlyingType = underlying ?? PropertyType; - PropertyTypeCode = Type.GetTypeCode(UnderlyingType); - - // Pre-compute: is this a complex type that needs recursive handling? - IsComplexType = !IsPrimitiveOrStringFast(PropertyType); - - _dynamicGetter = AcSerializerCommon.CreateCompiledGetter(declaringType, prop); - - // Initialize typed getter - AccessorType = DetermineAccessorType(PropertyType); InitializeTypedGetter(declaringType, prop); } - private static PropertyAccessorType DetermineAccessorType(Type propType) - { - var underlying = Nullable.GetUnderlyingType(propType); - if (underlying != null) - return PropertyAccessorType.Object; - - if (propType.IsEnum) - return PropertyAccessorType.Enum; - - if (ReferenceEquals(propType, GuidType)) - return PropertyAccessorType.Guid; - - return Type.GetTypeCode(propType) switch - { - TypeCode.Int32 => PropertyAccessorType.Int32, - TypeCode.Int64 => PropertyAccessorType.Int64, - TypeCode.Boolean => PropertyAccessorType.Boolean, - TypeCode.Double => PropertyAccessorType.Double, - TypeCode.Single => PropertyAccessorType.Single, - TypeCode.Decimal => PropertyAccessorType.Decimal, - TypeCode.DateTime => PropertyAccessorType.DateTime, - TypeCode.Byte => PropertyAccessorType.Byte, - TypeCode.Int16 => PropertyAccessorType.Int16, - TypeCode.UInt16 => PropertyAccessorType.UInt16, - TypeCode.UInt32 => PropertyAccessorType.UInt32, - TypeCode.UInt64 => PropertyAccessorType.UInt64, - _ => PropertyAccessorType.Object - }; - } - private void InitializeTypedGetter(Type declaringType, PropertyInfo prop) { switch (AccessorType) @@ -207,12 +87,6 @@ public abstract class PropertyAccessorBase #region Typed Getters - Direct invocation, no cast! - /// - /// Gets the property value from the target object (boxed). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public object? GetDynamicValue(object obj) => _dynamicGetter(obj); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public int GetInt32(object obj) => _int32Getter!(obj); diff --git a/AyCode.Core/Serializers/PropertyMetadataBase.cs b/AyCode.Core/Serializers/PropertyMetadataBase.cs new file mode 100644 index 0000000..c6a2b06 --- /dev/null +++ b/AyCode.Core/Serializers/PropertyMetadataBase.cs @@ -0,0 +1,143 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using static AyCode.Core.Helpers.JsonUtilities; + +namespace AyCode.Core.Serializers; + +/// +/// Enum for typed property accessor dispatch. +/// +public enum PropertyAccessorType : byte +{ + Object = 0, + Int32, + Int64, + Boolean, + Double, + Single, + Decimal, + DateTime, + Byte, + Int16, + UInt16, + UInt32, + UInt64, + Guid, + Enum +} + +/// +/// Base class containing common property metadata shared by serializers and deserializers. +/// Contains the dynamic getter used by both serialize (for reading) and deserialize (for Populate/Merge). +/// +public abstract class PropertyMetadataBase +{ + /// + /// Property name. + /// + public string Name { get; } + + /// + /// Pre-encoded UTF8 bytes of property name for fast matching. + /// + public byte[] NameUtf8 { get; } + + /// + /// The property type (may be nullable). + /// + public Type PropertyType { get; } + + /// + /// The underlying type (unwrapped from Nullable if applicable). + /// + public Type UnderlyingType { get; } + + /// + /// Cached TypeCode for fast primitive type dispatch. + /// + public TypeCode PropertyTypeCode { get; } + + /// + /// Whether the property type is nullable. + /// + public bool IsNullable { get; } + + /// + /// The declaring type of this property. + /// + public Type DeclaringType { get; } + + /// + /// True if this property needs recursive scanning (not primitive/string). + /// Pre-computed to avoid IsPrimitiveOrStringFast() calls in hot path. + /// + public bool IsComplexType { get; } + + /// + /// The accessor type for fast typed getter/setter dispatch. + /// + public PropertyAccessorType AccessorType { get; } + + /// + /// Compiled getter delegate for reading property values (boxed). + /// Used by serialize (for reading values) and deserialize (for Populate/Merge to get existing references). + /// + protected readonly Func _dynamicGetter; + + protected PropertyMetadataBase(PropertyInfo prop, Type declaringType) + { + Name = prop.Name; + NameUtf8 = Encoding.UTF8.GetBytes(prop.Name); + DeclaringType = declaringType; + PropertyType = prop.PropertyType; + + var underlying = Nullable.GetUnderlyingType(PropertyType); + IsNullable = underlying != null; + UnderlyingType = underlying ?? PropertyType; + PropertyTypeCode = Type.GetTypeCode(UnderlyingType); + + // Pre-compute: is this a complex type that needs recursive handling? + IsComplexType = !IsPrimitiveOrStringFast(PropertyType); + + AccessorType = DetermineAccessorType(PropertyType); + _dynamicGetter = AcSerializerCommon.CreateCompiledGetter(declaringType, prop); + } + + /// + /// Gets the property value from the target object (boxed). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public object? GetValue(object obj) => _dynamicGetter(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static PropertyAccessorType DetermineAccessorType(Type propType) + { + var underlying = Nullable.GetUnderlyingType(propType); + if (underlying != null) + return PropertyAccessorType.Object; + + if (propType.IsEnum) + return PropertyAccessorType.Enum; + + if (ReferenceEquals(propType, GuidType)) + return PropertyAccessorType.Guid; + + return Type.GetTypeCode(propType) switch + { + TypeCode.Int32 => PropertyAccessorType.Int32, + TypeCode.Int64 => PropertyAccessorType.Int64, + TypeCode.Boolean => PropertyAccessorType.Boolean, + TypeCode.Double => PropertyAccessorType.Double, + TypeCode.Single => PropertyAccessorType.Single, + TypeCode.Decimal => PropertyAccessorType.Decimal, + TypeCode.DateTime => PropertyAccessorType.DateTime, + TypeCode.Byte => PropertyAccessorType.Byte, + TypeCode.Int16 => PropertyAccessorType.Int16, + TypeCode.UInt16 => PropertyAccessorType.UInt16, + TypeCode.UInt32 => PropertyAccessorType.UInt32, + TypeCode.UInt64 => PropertyAccessorType.UInt64, + _ => PropertyAccessorType.Object + }; + } +} diff --git a/AyCode.Core/Serializers/PropertySetterBase.cs b/AyCode.Core/Serializers/PropertySetterBase.cs index a16c5d1..f81410a 100644 --- a/AyCode.Core/Serializers/PropertySetterBase.cs +++ b/AyCode.Core/Serializers/PropertySetterBase.cs @@ -6,10 +6,12 @@ using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers; /// -/// Base class for property accessors that also support setting values. -/// Used by deserializers. Extends PropertyAccessorBase with typed setters and IId collection support. +/// Base class for property setters used by deserializers. +/// Derives from PropertyMetadataBase (not PropertyAccessorBase) to avoid unnecessary typed getter delegates. +/// Contains setter functionality and typed setter delegates. +/// Inherits GetValue() from PropertyMetadataBase for Populate/Merge operations. /// -public abstract class PropertySetterBase : PropertyAccessorBase +public abstract class PropertySetterBase : PropertyMetadataBase { /// /// Compiled setter delegate for writing property values (boxed). diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.DataSection.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.DataSection.cs index 3a5a96d..913adc0 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializer.DataSection.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.DataSection.cs @@ -247,7 +247,7 @@ public static partial class AcToonSerializer // Write properties foreach (var prop in metadata.Properties) { - var propValue = prop.GetDynamicValue(value); + var propValue = prop.GetValue(value); // Skip null/default values if option is set if (context.Options.OmitDefaultValues && prop.IsDefaultValue(propValue)) diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.cs index 928e613..406502d 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializer.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.cs @@ -313,7 +313,7 @@ public static partial class AcToonSerializer var metadata = GetTypeMetadata(type); foreach (var prop in metadata.Properties) { - var propValue = prop.GetDynamicValue(value); + var propValue = prop.GetValue(value); if (propValue != null) ScanReferences(propValue, context, depth + 1); } }