diff --git a/AyCode.Core.Serializers.Console/BenchmarkTestDataProvider.cs b/AyCode.Core.Serializers.Console/BenchmarkTestDataProvider.cs new file mode 100644 index 0000000..ab3a521 --- /dev/null +++ b/AyCode.Core.Serializers.Console/BenchmarkTestDataProvider.cs @@ -0,0 +1,234 @@ +using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Tests.TestModels; + +namespace AyCode.Core.Serializers.Console; + +internal static class BenchmarkTestDataProvider +{ + internal static List CreateTestDataSets() + { + return new List + { + CreateSmallTestData(), + CreateMediumTestData(), + CreateLargeTestData(), + CreateRepeatedStringsTestData(), + CreateDeepNestedTestData() + }; + } + + internal static TestOrder CreateProfilerOrder() + { + TestDataFactory.ResetIdCounter(); + var sharedTag = TestDataFactory.CreateTag("SharedTag"); + var sharedUser = TestDataFactory.CreateUser("shareduser"); + return TestDataFactory.CreateOrder( + itemCount: 3, + palletsPerItem: 3, + measurementsPerPallet: 3, + pointsPerMeasurement: 4, + sharedTag: sharedTag, + sharedUser: sharedUser); + } + + private static TestDataSet CreateSmallTestData() + { + TestDataFactory.ResetIdCounter(); + + var sharedTag = TestDataFactory.CreateTag("SharedTag"); + var sharedUser = TestDataFactory.CreateUser("shareduser"); + + var order = TestDataFactory.CreateOrder( + itemCount: 2, + palletsPerItem: 2, + measurementsPerPallet: 2, + pointsPerMeasurement: 2, + sharedTag: sharedTag, + sharedUser: sharedUser); + + ClearDeepLevelRefs(order); + + return new TestDataSet("Small (2x2x2x2)", order, iidRefPercent: 10); + } + + private static TestDataSet CreateMediumTestData() + { + TestDataFactory.ResetIdCounter(); + + var sharedTag = TestDataFactory.CreateTag("SharedTag"); + var sharedUser = TestDataFactory.CreateUser("shareduser"); + var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true); + + var sharedPreferences = new UserPreferences + { + Theme = "dark", + Language = "en-US", + NotificationsEnabled = true, + EmailDigestFrequency = "weekly" + }; + sharedUser.Preferences = sharedPreferences; + + var order = TestDataFactory.CreateOrder( + itemCount: 3, + palletsPerItem: 3, + measurementsPerPallet: 3, + pointsPerMeasurement: 4, + sharedTag: sharedTag, + sharedUser: sharedUser, + sharedMetadata: sharedMeta, + sharedPreferences: sharedPreferences); + + ClearDeepLevelRefs(order); + + return new TestDataSet("Medium (3x3x3x4)", order, iidRefPercent: 10); + } + + private static TestDataSet CreateLargeTestData() + { + TestDataFactory.ResetIdCounter(); + + var sharedTag = TestDataFactory.CreateTag("SharedTag"); + var sharedUser = TestDataFactory.CreateUser("shareduser"); + + var sharedPreferences = new UserPreferences + { + Theme = "light", + Language = "de-DE", + NotificationsEnabled = false, + EmailDigestFrequency = "daily" + }; + sharedUser.Preferences = sharedPreferences; + + var order = TestDataFactory.CreateOrder( + itemCount: 5, + palletsPerItem: 5, + measurementsPerPallet: 5, + pointsPerMeasurement: 10, + sharedTag: sharedTag, + sharedUser: sharedUser, + sharedPreferences: sharedPreferences); + + ClearDeepLevelRefs(order); + + return new TestDataSet("Large (5x5x5x10)", order, iidRefPercent: 10); + } + + private static TestDataSet CreateRepeatedStringsTestData() + { + TestDataFactory.ResetIdCounter(); + + var sharedTag = TestDataFactory.CreateTag("RepeatedTag"); + var sharedUser = TestDataFactory.CreateUser("repeateduser"); + + var sharedPreferences = new UserPreferences + { + Theme = "dark", + Language = "en-US", + NotificationsEnabled = true, + EmailDigestFrequency = "weekly" + }; + sharedUser.Preferences = sharedPreferences; + + var order = TestDataFactory.CreateOrder( + itemCount: 10, + palletsPerItem: 2, + measurementsPerPallet: 2, + pointsPerMeasurement: 2, + sharedTag: sharedTag, + sharedUser: sharedUser, + sharedPreferences: sharedPreferences); + + foreach (var item in order.Items) + { + item.Status = TestStatus.Processing; + item.ProductName = "CommonProductName_RepeatedForTesting"; + } + + ClearDeepLevelRefs(order); + + return new TestDataSet("Repeated Strings (10 items)", order, iidRefPercent: 10); + } + + private static TestDataSet CreateDeepNestedTestData() + { + TestDataFactory.ResetIdCounter(); + + var sharedTag = TestDataFactory.CreateTag("DeepTag"); + var sharedUser = TestDataFactory.CreateUser("deepuser"); + var sharedCategory = TestDataFactory.CreateCategory("DeepCategory"); + + var sharedPreferences = new UserPreferences + { + Theme = "light", + Language = "fr-FR", + NotificationsEnabled = false, + EmailDigestFrequency = "monthly" + }; + sharedUser.Preferences = sharedPreferences; + + var order = TestDataFactory.CreateOrder( + itemCount: 2, + palletsPerItem: 4, + measurementsPerPallet: 4, + pointsPerMeasurement: 8, + sharedTag: sharedTag, + sharedUser: sharedUser, + sharedPreferences: sharedPreferences, + sharedCategory: sharedCategory); + + ClearDeepLevelRefs(order); + + return new TestDataSet("Deep Nested (2x4x4x8)", order, iidRefPercent: 10); + } + + private static void ClearDeepLevelRefs(TestOrder order) + { + foreach (var item in order.Items) + { + foreach (var pallet in item.Pallets) + { + pallet.Tag = null; + pallet.Inspector = null; + pallet.Category = null; + + foreach (var measurement in pallet.Measurements) + { + measurement.Tag = null; + measurement.Operator = null; + + foreach (var point in measurement.Points) + { + point.Tag = null; + point.Verifier = null; + } + } + } + } + } +} + +internal sealed class TestDataSet +{ + public string Name { get; } + public TestOrder Order { get; } + + /// + /// Percentage of IId shared references in the data (0-100). + /// Higher values mean more deduplication benefit for Default mode. + /// + public int IIdRefPercent { get; } + + public TestDataSet(string name, TestOrder order, int iidRefPercent = 0) + { + Name = name; + Order = order; + IIdRefPercent = iidRefPercent; + } + + /// + /// Gets display name including IId ref percentage if set. + /// + public string DisplayName => IIdRefPercent > 0 + ? $"{Name} [{IIdRefPercent}% IId refs]" + : Name; +} diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index 6a5ee75..4da051a 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -68,7 +68,7 @@ public static class Program } // Profiler mode: warmup only, then exit (for memory profiler analysis) - //if (mode == "profiler") + if (mode == "profiler") { RunProfilerMode(); return; @@ -82,7 +82,7 @@ public static class Program System.Console.WriteLine(); var allResults = new List(); - var testDataSets = CreateTestDataSets(); + var testDataSets = BenchmarkTestDataProvider.CreateTestDataSets(); foreach (var testData in testDataSets) { @@ -115,17 +115,7 @@ public static class Program System.Console.WriteLine($"Build: {BuildConfiguration} | .NET: {Environment.Version}"); System.Console.WriteLine(); - // Create medium test data - TestDataFactory.ResetIdCounter(); - var sharedTag = TestDataFactory.CreateTag("SharedTag"); - var sharedUser = TestDataFactory.CreateUser("shareduser"); - var order = TestDataFactory.CreateOrder( - itemCount: 3, - palletsPerItem: 3, - measurementsPerPallet: 3, - pointsPerMeasurement: 4, - sharedTag: sharedTag, - sharedUser: sharedUser); + var order = BenchmarkTestDataProvider.CreateProfilerOrder(); var options = AcBinarySerializerOptions.WithoutReferenceHandling; options.UseStringInterning = StringInterningMode.None; @@ -167,217 +157,6 @@ public static class Program System.Console.WriteLine("✓ Profiler mode complete. Exiting now."); } - #region Test Data Creation - - private static List CreateTestDataSets() - { - return new List - { - CreateSmallTestData(), - CreateMediumTestData(), - CreateLargeTestData(), - CreateRepeatedStringsTestData(), - CreateDeepNestedTestData() - }; - } - - private static TestDataSet CreateSmallTestData() - { - TestDataFactory.ResetIdCounter(); - - // Create shared references - IId types (only at Order/Item level) - var sharedTag = TestDataFactory.CreateTag("SharedTag"); - var sharedUser = TestDataFactory.CreateUser("shareduser"); - - var order = TestDataFactory.CreateOrder( - itemCount: 2, - palletsPerItem: 2, - measurementsPerPallet: 2, - pointsPerMeasurement: 2, - sharedTag: sharedTag, - sharedUser: sharedUser); - - // Clear deeper level refs for realistic ~10% ratio - ClearDeepLevelRefs(order); - - return new TestDataSet("Small (2x2x2x2)", order, iidRefPercent: 10); - } - - private static TestDataSet CreateMediumTestData() - { - TestDataFactory.ResetIdCounter(); - - // IId shared references - var sharedTag = TestDataFactory.CreateTag("SharedTag"); - var sharedUser = TestDataFactory.CreateUser("shareduser"); - var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true); - - // Non-IId shared reference - create separate preferences for 2 users - var sharedPreferences = new UserPreferences - { - Theme = "dark", - Language = "en-US", - NotificationsEnabled = true, - EmailDigestFrequency = "weekly" - }; - sharedUser.Preferences = sharedPreferences; - - var order = TestDataFactory.CreateOrder( - itemCount: 3, - palletsPerItem: 3, - measurementsPerPallet: 3, - pointsPerMeasurement: 4, - sharedTag: sharedTag, - sharedUser: sharedUser, - sharedMetadata: sharedMeta, - sharedPreferences: sharedPreferences); - - // Clear deeper level refs for realistic ~10% ratio - ClearDeepLevelRefs(order); - - return new TestDataSet("Medium (3x3x3x4)", order, iidRefPercent: 10); - } - - private static TestDataSet CreateLargeTestData() - { - TestDataFactory.ResetIdCounter(); - - // IId shared references - var sharedTag = TestDataFactory.CreateTag("SharedTag"); - var sharedUser = TestDataFactory.CreateUser("shareduser"); - - // Non-IId shared reference - var sharedPreferences = new UserPreferences - { - Theme = "light", - Language = "de-DE", - NotificationsEnabled = false, - EmailDigestFrequency = "daily" - }; - sharedUser.Preferences = sharedPreferences; - - var order = TestDataFactory.CreateOrder( - itemCount: 5, - palletsPerItem: 5, - measurementsPerPallet: 5, - pointsPerMeasurement: 10, - sharedTag: sharedTag, - sharedUser: sharedUser, - sharedPreferences: sharedPreferences); - - // Clear deeper level refs for realistic ~10% ratio - ClearDeepLevelRefs(order); - - return new TestDataSet("Large (5x5x5x10)", order, iidRefPercent: 10); - } - - private static TestDataSet CreateRepeatedStringsTestData() - { - TestDataFactory.ResetIdCounter(); - - // IId shared references - var sharedTag = TestDataFactory.CreateTag("RepeatedTag"); - var sharedUser = TestDataFactory.CreateUser("repeateduser"); - - // Non-IId shared reference - var sharedPreferences = new UserPreferences - { - Theme = "dark", - Language = "en-US", - NotificationsEnabled = true, - EmailDigestFrequency = "weekly" - }; - sharedUser.Preferences = sharedPreferences; - - // Create order with many items to test string interning on repeated property names - var order = TestDataFactory.CreateOrder( - itemCount: 10, - palletsPerItem: 2, - measurementsPerPallet: 2, - pointsPerMeasurement: 2, - sharedTag: sharedTag, - sharedUser: sharedUser, - sharedPreferences: sharedPreferences); - - // Set same status and ProductName on all items to test enum and string handling - foreach (var item in order.Items) - { - item.Status = TestStatus.Processing; - item.ProductName = "CommonProductName_RepeatedForTesting"; - } - - // Clear deeper level refs for realistic ~10% ratio - ClearDeepLevelRefs(order); - - return new TestDataSet("Repeated Strings (10 items)", order, iidRefPercent: 10); - } - - /// - /// Clears IId shared references from Pallet, Measurement, and Point levels. - /// This creates a realistic ~10% IId ref ratio (only Order and Item levels have refs). - /// - private static void ClearDeepLevelRefs(TestOrder order) - { - foreach (var item in order.Items) - { - foreach (var pallet in item.Pallets) - { - pallet.Tag = null; - pallet.Inspector = null; - pallet.Category = null; - - foreach (var measurement in pallet.Measurements) - { - measurement.Tag = null; - measurement.Operator = null; - - foreach (var point in measurement.Points) - { - point.Tag = null; - point.Verifier = null; - } - } - } - } - } - - private static TestDataSet CreateDeepNestedTestData() - { - TestDataFactory.ResetIdCounter(); - - // IId shared references - only at Order and Item levels for ~10% ratio - var sharedTag = TestDataFactory.CreateTag("DeepTag"); - var sharedUser = TestDataFactory.CreateUser("deepuser"); - var sharedCategory = TestDataFactory.CreateCategory("DeepCategory"); - - // Non-IId shared reference - var sharedPreferences = new UserPreferences - { - Theme = "light", - Language = "fr-FR", - NotificationsEnabled = false, - EmailDigestFrequency = "monthly" - }; - sharedUser.Preferences = sharedPreferences; - - var order = TestDataFactory.CreateOrder( - itemCount: 2, - palletsPerItem: 4, - measurementsPerPallet: 4, - pointsPerMeasurement: 8, - sharedTag: sharedTag, - sharedUser: sharedUser, - sharedPreferences: sharedPreferences, - sharedCategory: sharedCategory); - - // Clear deeper level refs for realistic ~10% ratio - ClearDeepLevelRefs(order); - - return new TestDataSet("Deep Nested (2x4x4x8)", order, iidRefPercent: 10); - } - - #endregion - #region Benchmark Execution private static List RunBenchmarksForTestData(TestDataSet testData, string mode) @@ -556,8 +335,11 @@ public static class Program { _order = order; Name = name; - _options = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); + + //_options = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); //_options = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.Lz4Block); + _options = MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.None); + _serialized = MessagePackSerializer.Serialize(order, _options); } @@ -660,34 +442,6 @@ public static class Program #region Results - private sealed class TestDataSet - { - public string Name { get; } - public TestOrder Order { get; } - - /// - /// Percentage of IId shared references in the data (0-100). - /// Higher values mean more deduplication benefit for Default mode. - /// - public int IIdRefPercent { get; } - - public TestDataSet(string name, TestOrder order, int iidRefPercent = 0) - { - Name = name; - Order = order; - IIdRefPercent = iidRefPercent; - } - - /// - /// Gets display name including IId ref percentage if set. - /// - public string DisplayName => IIdRefPercent > 0 - ? $"{Name} [{IIdRefPercent}% IId refs]" - : Name; - } - - - private sealed class BenchmarkResult { public string TestDataName { get; set; } = ""; diff --git a/AyCode.Core.Tests/TestModels/SharedTestModels.cs b/AyCode.Core.Tests/TestModels/SharedTestModels.cs index c22959e..3f04e1c 100644 --- a/AyCode.Core.Tests/TestModels/SharedTestModels.cs +++ b/AyCode.Core.Tests/TestModels/SharedTestModels.cs @@ -53,14 +53,22 @@ public enum TestUserRole /// Implements IId<int> for semantic $id/$ref serialization. /// [AcBinarySerializable] +[MessagePackObject] public class SharedTag : IId { + [Key(0)] public int Id { get; set; } + [Key(1)] public string Name { get; set; } = ""; + [Key(2)] public string Color { get; set; } = "#000000"; + [Key(3)] public int Priority { get; set; } + [Key(4)] public bool IsActive { get; set; } = true; + [Key(5)] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + [Key(6)] public string? Description { get; set; } } @@ -68,15 +76,24 @@ public class SharedTag : IId /// Shared category - for hierarchical cross-reference testing. /// [AcBinarySerializable] +[MessagePackObject] public class SharedCategory : IId { + [Key(0)] public int Id { get; set; } + [Key(1)] public string Name { get; set; } = ""; + [Key(2)] public string? Description { get; set; } + [Key(3)] public int SortOrder { get; set; } + [Key(4)] public bool IsDefault { get; set; } + [Key(5)] public int? ParentCategoryId { get; set; } + [Key(6)] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + [Key(7)] public DateTime? UpdatedAt { get; set; } } @@ -84,17 +101,28 @@ public class SharedCategory : IId /// Shared user reference - appears in many places to test $ref deduplication. /// [AcBinarySerializable] +[MessagePackObject] public class SharedUser : IId { + [Key(0)] public int Id { get; set; } + [Key(1)] public string Username { get; set; } = ""; + [Key(2)] public string Email { get; set; } = ""; + [Key(3)] public string FirstName { get; set; } = ""; + [Key(4)] public string LastName { get; set; } = ""; + [Key(5)] public bool IsActive { get; set; } = true; + [Key(6)] public TestUserRole Role { get; set; } = TestUserRole.User; + [Key(7)] public DateTime? LastLoginAt { get; set; } + [Key(8)] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + [Key(9)] public UserPreferences? Preferences { get; set; } } @@ -102,11 +130,16 @@ public class SharedUser : IId /// User preferences - non-IId nested object /// [AcBinarySerializable] +[MessagePackObject] public class UserPreferences { + [Key(0)] public string Theme { get; set; } = "light"; + [Key(1)] public string Language { get; set; } = "en-US"; + [Key(2)] public bool NotificationsEnabled { get; set; } = true; + [Key(3)] public string? EmailDigestFrequency { get; set; } } @@ -119,15 +152,20 @@ public class UserPreferences /// Does NOT implement IId, so uses standard Newtonsoft reference tracking. /// [AcBinarySerializable] +[MessagePackObject] public class MetadataInfo { + [Key(0)] public string Key { get; set; } = ""; + [Key(1)] public string Value { get; set; } = ""; + [Key(2)] public DateTime Timestamp { get; set; } = DateTime.UtcNow; /// /// Nested metadata for deep Newtonsoft reference testing /// + [Key(3)] public MetadataInfo? ChildMetadata { get; set; } } @@ -139,34 +177,51 @@ public class MetadataInfo /// Level 1: Main order - root of the hierarchy /// [AcBinarySerializable] +[MessagePackObject] public class TestOrder : IId { + [Key(0)] public int Id { get; set; } + [Key(1)] public string OrderNumber { get; set; } = ""; + [Key(2)] public TestStatus Status { get; set; } = TestStatus.Pending; + [Key(3)] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + [Key(4)] public DateTime? PaidDateUtc { get; set; } + [Key(5)] public decimal TotalAmount { get; set; } // Level 2 collection + [Key(6)] public List Items { get; set; } = []; // Shared reference properties (for $id/$ref testing) + [Key(7)] public SharedTag? PrimaryTag { get; set; } + [Key(8)] public SharedTag? SecondaryTag { get; set; } + [Key(9)] public SharedUser? Owner { get; set; } + [Key(10)] public SharedCategory? Category { get; set; } // Collection of shared references + [Key(11)] public List Tags { get; set; } = []; // Non-IId metadata (for Newtonsoft $ref testing) + [Key(12)] public MetadataInfo? OrderMetadata { get; set; } + [Key(13)] public MetadataInfo? AuditMetadata { get; set; } + [Key(14)] public List MetadataList { get; set; } = []; // NoMerge collection for testing replace behavior [JsonNoMergeCollection] + [Key(15)] public List NoMergeItems { get; set; } = []; // Parent reference - ignored by all serializers to prevent circular references @@ -180,20 +235,30 @@ public class TestOrder : IId /// Level 2: Order item with pallets /// [AcBinarySerializable] +[MessagePackObject] public class TestOrderItem : IId { + [Key(0)] public int Id { get; set; } + [Key(1)] public string ProductName { get; set; } = ""; + [Key(2)] public int Quantity { get; set; } + [Key(3)] public decimal UnitPrice { get; set; } + [Key(4)] public TestStatus Status { get; set; } = TestStatus.Pending; // Level 3 collection + [Key(5)] public List Pallets { get; set; } = []; // Shared references + [Key(6)] public SharedTag? Tag { get; set; } + [Key(7)] public SharedUser? Assignee { get; set; } + [Key(8)] public MetadataInfo? ItemMetadata { get; set; } // Parent reference - ignored by all serializers to prevent circular references @@ -207,23 +272,34 @@ public class TestOrderItem : IId /// Level 3: Pallet containing measurements /// [AcBinarySerializable] +[MessagePackObject] public class TestPallet : IId { + [Key(0)] public int Id { get; set; } + [Key(1)] public string PalletCode { get; set; } = ""; + [Key(2)] public int TrayCount { get; set; } + [Key(3)] public TestStatus Status { get; set; } = TestStatus.Pending; + [Key(4)] public double Weight { get; set; } // Level 4 collection + [Key(5)] public List Measurements { get; set; } = []; // Shared IId references for better reference testing + [Key(6)] public SharedTag? Tag { get; set; } + [Key(7)] public SharedUser? Inspector { get; set; } + [Key(8)] public SharedCategory? Category { get; set; } // Non-IId shared references + [Key(9)] public MetadataInfo? PalletMetadata { get; set; } // Parent reference - ignored by all serializers to prevent circular references @@ -237,18 +313,26 @@ public class TestPallet : IId /// Level 4: Measurement with multiple points /// [AcBinarySerializable] +[MessagePackObject] public class TestMeasurement : IId { + [Key(0)] public int Id { get; set; } + [Key(1)] public string Name { get; set; } = ""; + [Key(2)] public double TotalWeight { get; set; } + [Key(3)] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; // Level 5 collection + [Key(4)] public List Points { get; set; } = []; // Shared IId references for better reference testing + [Key(5)] public SharedTag? Tag { get; set; } + [Key(6)] public SharedUser? Operator { get; set; } // Parent reference - ignored by all serializers to prevent circular references @@ -262,15 +346,22 @@ public class TestMeasurement : IId /// Level 5: Deepest level - measurement point /// [AcBinarySerializable] +[MessagePackObject] public class TestMeasurementPoint : IId { + [Key(0)] public int Id { get; set; } + [Key(1)] public string Label { get; set; } = ""; + [Key(2)] public double Value { get; set; } + [Key(3)] public DateTime MeasuredAt { get; set; } = DateTime.UtcNow; // Shared IId reference for better reference testing (many points share same tag/user) + [Key(4)] public SharedTag? Tag { get; set; } + [Key(5)] public SharedUser? Verifier { get; set; } // Parent reference - ignored by all serializers to prevent circular references diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs index 3b5ad3a..a2c3636 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs @@ -415,14 +415,15 @@ public static partial class AcBinaryDeserializer if (stringCache.TryGetValue(hash, out var cached)) { - // Hash includes all bytes for short strings, so collision is extremely unlikely - // For longer strings, we still verify length as a sanity check - if (cached.Length == length) + // CRITICAL: Verify actual content matches to prevent hash collision data corruption. + // cached.Length is char count, length is UTF-8 byte count — equal only for ASCII. + // For ASCII strings (common case): byte-by-byte comparison against UTF-8 buffer. + if (cached.Length == length && VerifyAsciiUtf8Match(cached, slice)) { _position += length; return cached; } - // Hash collision with different length - fall through to read new value + // Hash collision or non-ASCII length mismatch - fall through to decode } var value = Utf8NoBom.GetString(slice); @@ -431,6 +432,22 @@ public static partial class AcBinaryDeserializer return value; } + /// + /// Verifies that a cached ASCII string matches the UTF-8 bytes exactly. + /// For ASCII, each char's low byte equals the UTF-8 byte. + /// Caller guarantees cached.Length == utf8Bytes.Length (ASCII invariant). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool VerifyAsciiUtf8Match(string cached, ReadOnlySpan utf8Bytes) + { + for (var i = 0; i < cached.Length; i++) + { + if ((byte)cached[i] != utf8Bytes[i]) + return false; + } + return true; + } + /// /// Compute hash that includes ALL bytes for short strings to avoid collisions. /// diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs index 85db77a..8d47eed 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs @@ -155,8 +155,7 @@ public static partial class AcBinaryDeserializer var positionBeforeRead = context.Position; try { - // Use typed setters for primitives to avoid boxing - // Skip method call for Object/String/Collection types - they can't use typed setters + // Use typed setters for primitives and strings to avoid ReadValue dispatch if (propInfo.AccessorType != PropertyAccessorType.Object && TryReadAndSetTypedValue(ref context, target, propInfo, peekCode)) continue; diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index f92f6f3..adb8a03 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -915,6 +915,40 @@ public static partial class AcBinaryDeserializer return true; } break; + + case PropertyAccessorType.String: + if (BinaryTypeCode.IsFixStr(peekCode)) + { + context.ReadByte(); + var length = BinaryTypeCode.DecodeFixStrLength(peekCode); + propInfo.SetValue(target, length == 0 ? string.Empty : context.ReadStringUtf8(length)); + return true; + } + if (peekCode == BinaryTypeCode.String) + { + context.ReadByte(); + propInfo.SetValue(target, ReadPlainString(ref context)); + return true; + } + if (peekCode == BinaryTypeCode.StringEmpty) + { + context.ReadByte(); + propInfo.SetValue(target, string.Empty); + return true; + } + if (peekCode == BinaryTypeCode.StringInterned) + { + context.ReadByte(); + propInfo.SetValue(target, context.GetInternedString((int)context.ReadVarUInt())); + return true; + } + if (peekCode == BinaryTypeCode.StringInternFirst) + { + context.ReadByte(); + propInfo.SetValue(target, ReadAndRegisterInternedString(ref context)); + return true; + } + break; } return false; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 5f8775f..20f8394 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -412,6 +412,24 @@ public static partial class AcBinarySerializer if (TryWritePrimitive(value, type, context)) return; + WriteValueNonPrimitive(value, type, context, depth); + } + + /// + /// Writes a non-primitive value (collection, dictionary, byte[], or complex object). + /// Skips null check and TryWritePrimitive — caller guarantees value is non-null and not a primitive type. + /// Called from WritePropertyOrSkip default case (PropertyAccessorType.Object) and WriteValue fallback. + /// + private static void WriteValueNonPrimitive(object value, Type type, BinarySerializationContext context, int depth) + { + // Nullable where T is a value type: boxed value may be a primitive. + // Only Nullable can be a value type in the Object accessor path. + if (type.IsValueType) + { + if (TryWritePrimitive(value, value.GetType(), context)) + return; + } + if (depth > context.MaxDepth) { context.WriteByte(BinaryTypeCode.Null); @@ -1243,7 +1261,8 @@ public static partial class AcBinarySerializer } default: { - // Object type - use regular getter + // Object type (collection, complex object, byte[], dictionary) + // TryWritePrimitive is always false for these — skip it via WriteValueNonPrimitive var value = prop.GetValue(obj); // SKIP marker only for null (reference types) @@ -1257,7 +1276,7 @@ public static partial class AcBinarySerializer #if DEBUG context.CurrentPropertyPath = $"{prop.DeclaringType.Name}.{prop.Name}"; #endif - WriteValue(value, prop.PropertyType, context, depth); + WriteValueNonPrimitive(value, prop.PropertyType, context, depth); } return; } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index f58ef46..927b822 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -82,7 +82,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// allowing the deserializer to match properties by name between different types. /// Default: false (no overhead) /// - public bool UseMetadata { get; init; } = false; + public bool UseMetadata { get; init; } = true; /// /// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).