diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index a645910..eeb06d6 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -50,7 +50,7 @@ public static class Program private static int TestIterations = 1; #else private static int WarmupIterations = 5000; - private static int TestIterations = 1000; + private static int TestIterations = 5000; //private static int WarmupIterations = 5000; //private static int TestIterations = 2000; diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs index b401f03..407bd1d 100644 --- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs @@ -193,8 +193,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator collKind = origDef switch { "System.Collections.Generic.List" => "List", - "System.Collections.Generic.IList" => "List", // has Count + indexer - "System.Collections.Generic.IReadOnlyList" => "List", // has Count + indexer + "System.Collections.Generic.IList" => "IndexedCollection", + "System.Collections.Generic.IReadOnlyList" => "IndexedCollection", "System.Collections.Generic.HashSet" => "Counted", // has Count, no indexer "System.Collections.Generic.Queue" => "Counted", "System.Collections.Generic.ICollection" => "Counted", @@ -304,6 +304,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine("// "); sb.AppendLine("#nullable enable"); sb.AppendLine("using System.Runtime.CompilerServices;"); + sb.AppendLine("using System.Runtime.InteropServices;"); sb.AppendLine("using AyCode.Core.Serializers.Binaries;"); // ReferenceHandlingMode is needed for ScanObject self ref tracking and direct object write/scan sb.AppendLine("using AyCode.Core.Serializers;"); @@ -753,6 +754,13 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"{i} var se_{p.Name} = scol_{p.Name}[si_{p.Name}];"); } else if (p.CollectionKind == "List") + { + sb.AppendLine($"{i} var span_{p.Name} = CollectionsMarshal.AsSpan(scol_{p.Name});"); + sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < span_{p.Name}.Length; si_{p.Name}++)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} var se_{p.Name} = span_{p.Name}[si_{p.Name}];"); + } + else if (p.CollectionKind == "IndexedCollection") { sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Count; si_{p.Name}++)"); sb.AppendLine($"{i} {{"); @@ -812,6 +820,13 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"{i} var se_{p.Name} = scol_{p.Name}[si_{p.Name}];"); } else if (p.CollectionKind == "List") + { + sb.AppendLine($"{i} var span_{p.Name} = CollectionsMarshal.AsSpan(scol_{p.Name});"); + sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < span_{p.Name}.Length; si_{p.Name}++)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} var se_{p.Name} = span_{p.Name}[si_{p.Name}];"); + } + else if (p.CollectionKind == "IndexedCollection") { sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Count; si_{p.Name}++)"); sb.AppendLine($"{i} {{"); @@ -1025,14 +1040,23 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"{i} foreach (var elem_{p.Name} in col_{p.Name})"); sb.AppendLine($"{i} {{"); } - else // List, IList, IReadOnlyList — Count + indexer + else if (p.CollectionKind == "IndexedCollection") { - sb.AppendLine($"{i} var list_{p.Name} = {a};"); - sb.AppendLine($"{i} context.WriteVarUInt((uint)list_{p.Name}.Count);"); + sb.AppendLine($"{i} var col_{p.Name} = {a};"); + sb.AppendLine($"{i} context.WriteVarUInt((uint)col_{p.Name}.Count);"); sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 1;"); - sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < list_{p.Name}.Count; i_{p.Name}++)"); + sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < col_{p.Name}.Count; i_{p.Name}++)"); sb.AppendLine($"{i} {{"); - sb.AppendLine($"{i} var elem_{p.Name} = list_{p.Name}[i_{p.Name}];"); + sb.AppendLine($"{i} var elem_{p.Name} = col_{p.Name}[i_{p.Name}];"); + } + else // List — CollectionsMarshal.AsSpan for zero-overhead iteration + { + sb.AppendLine($"{i} var span_{p.Name} = CollectionsMarshal.AsSpan({a});"); + sb.AppendLine($"{i} context.WriteVarUInt((uint)span_{p.Name}.Length);"); + sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 1;"); + sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < span_{p.Name}.Length; i_{p.Name}++)"); + sb.AppendLine($"{i} {{"); + sb.AppendLine($"{i} var elem_{p.Name} = span_{p.Name}[i_{p.Name}];"); } // Per-element write @@ -1542,7 +1566,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator EmitReadCollectionElement(sb, reader, elemCast, $"ri_{s}", s, i + " ", isArray: true, p.ElementNeedsRefScan); sb.AppendLine($"{i} }}"); } - else // List, Counted — all use List with Add + else // List, IndexedCollection, Counted — all use List with Add { sb.AppendLine($"{i} var col_{s} = new System.Collections.Generic.List<{elemType}>(cnt_{s});"); sb.AppendLine($"{i} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)"); @@ -2042,7 +2066,7 @@ internal sealed class PropInfo public string? ElementWriterClassName { get; } /// Id type name for collection element IId types. Null if not IId. public string? ElementIdTypeName { get; } - /// Collection type: "List", "Array", or null (unknown — fallback to runtime). + /// Collection type: "List", "Array", "IndexedCollection", "Counted", or null (unknown — fallback to runtime). public string? CollectionKind { get; } /// Full element type name for generated code (e.g. "SharedTag"). public string? ElementFullTypeName { get; } diff --git a/AyCode.Core.Tests/TestModels/SharedTestModels.cs b/AyCode.Core.Tests/TestModels/SharedTestModels.cs index 0d4f97e..11129a1 100644 --- a/AyCode.Core.Tests/TestModels/SharedTestModels.cs +++ b/AyCode.Core.Tests/TestModels/SharedTestModels.cs @@ -55,7 +55,7 @@ public enum TestUserRole /// Implements IId<int> for semantic $id/$ref serialization. /// [MemoryPackable] -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] [MessagePackObject] public partial class SharedTag : IId { @@ -80,7 +80,7 @@ public partial class SharedTag : IId /// Shared category - for hierarchical cross-reference testing. /// [MemoryPackable] -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] [MessagePackObject] public partial class SharedCategory : IId { @@ -106,7 +106,7 @@ public partial class SharedCategory : IId /// Shared user reference - appears in many places to test $ref deduplication. /// [MemoryPackable] -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] [MessagePackObject] public partial class SharedUser : IId { @@ -136,7 +136,7 @@ public partial class SharedUser : IId /// User preferences - non-IId nested object /// [MemoryPackable] -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] [MessagePackObject] public partial class UserPreferences { @@ -162,7 +162,7 @@ public partial class UserPreferences /// Does NOT implement IId, so uses standard Newtonsoft reference tracking. /// [MemoryPackable] -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] [MessagePackObject] public partial class MetadataInfo { @@ -190,7 +190,7 @@ public partial class MetadataInfo /// Level 1: Main order - root of the hierarchy /// [MemoryPackable] -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] [MessagePackObject] public partial class TestOrder : IId { @@ -250,7 +250,7 @@ public partial class TestOrder : IId /// Level 2: Order item with pallets /// [MemoryPackable] -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] [MessagePackObject] public partial class TestOrderItem : IId { @@ -290,7 +290,7 @@ public partial class TestOrderItem : IId /// Level 3: Pallet containing measurements /// [MemoryPackable] -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] [MessagePackObject] public partial class TestPallet : IId { @@ -333,7 +333,7 @@ public partial class TestPallet : IId /// Level 4: Measurement with multiple points /// [MemoryPackable] -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] [MessagePackObject] public partial class TestMeasurement : IId { @@ -368,7 +368,7 @@ public partial class TestMeasurement : IId /// Level 5: Deepest level - measurement point /// [MemoryPackable] -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] [MessagePackObject] public partial class TestMeasurementPoint : IId { @@ -402,7 +402,7 @@ public partial class TestMeasurementPoint : IId /// /// Order with Guid Id - for testing Guid-based IId /// -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] public class TestGuidOrder : IId { public Guid Id { get; set; } @@ -414,7 +414,7 @@ public class TestGuidOrder : IId /// /// Item with Guid Id /// -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] public class TestGuidItem : IId { public Guid Id { get; set; } @@ -430,7 +430,7 @@ public class TestGuidItem : IId /// Simulates NopCommerce GenericAttribute - stores key-value pairs where DateTime values /// are stored as strings in the database. /// -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] public class TestGenericAttribute { public int Id { get; set; } @@ -442,7 +442,7 @@ public class TestGenericAttribute /// DTO with GenericAttributes collection - simulates OrderDto with string-stored DateTime values. /// This reproduces the production bug where Binary serialization was thought to corrupt DateTime strings. /// -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] public class TestDtoWithGenericAttributes : IId { public int Id { get; set; } @@ -453,7 +453,7 @@ public class TestDtoWithGenericAttributes : IId /// /// Order with nullable collections for null vs empty testing /// -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] public class TestOrderWithNullableCollections { public int Id { get; set; } @@ -466,7 +466,7 @@ public class TestOrderWithNullableCollections /// Class with all primitive types for WASM/serialization testing /// [MemoryPackable] -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] public partial class PrimitiveTestClass { public int IntValue { get; set; } @@ -489,7 +489,7 @@ public partial class PrimitiveTestClass /// Class with extended primitive types for full serializer coverage. /// Includes DateTimeOffset, TimeSpan, Dictionary, null properties. /// -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] public class ExtendedPrimitiveTestClass { public int Id { get; set; } @@ -519,7 +519,7 @@ public class ExtendedPrimitiveTestClass /// /// Class with array of objects containing null items for WriteNull coverage /// -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] public class ObjectWithNullItems { public int Id { get; set; } @@ -534,7 +534,7 @@ public class ObjectWithNullItems /// "Server-side" DTO with extra properties that the "client" doesn't know about. /// Used to test SkipValue functionality when deserializing unknown properties. /// -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] public class ServerCustomerDto : IId { public int Id { get; set; } @@ -567,7 +567,7 @@ public class ServerCustomerDto : IId /// the deserializer must skip unknown properties correctly /// while still maintaining string intern table consistency. /// -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] public class ClientCustomerDto : IId { public int Id { get; set; } @@ -581,7 +581,7 @@ public class ClientCustomerDto : IId /// Server DTO with nested objects that client doesn't know about. /// Tests skipping complex nested structures. /// -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] public class ServerOrderWithExtras : IId { public int Id { get; set; } @@ -602,7 +602,7 @@ public class ServerOrderWithExtras : IId /// /// Client version of the order - doesn't have Customer/RelatedCustomers properties. /// -[AcBinarySerializable(false)] +[AcBinarySerializable(true)] public class ClientOrderSimple : IId { public int Id { get; set; }