From e30efff56c4a2a7bd062db91639ee61360e95c88 Mon Sep 17 00:00:00 2001 From: Loretta Date: Sun, 15 Feb 2026 19:01:21 +0100 Subject: [PATCH] Enable property-level string interning via attribute Introduce [AcStringIntern] for selective string interning on properties. Update serializer to use a per-property eligibility flag, improving efficiency and control over which string fields participate in interning during binary serialization. Update test models and internal context accordingly. --- AyCode.Core.Tests/TestModels/AcSerializerModels.cs | 3 +++ AyCode.Core.Tests/TestModels/SharedTestModels.cs | 8 ++++++++ .../AcBinarySerializer.BinarySerializationContext.cs | 8 ++++++++ .../Serializers/Binaries/AcBinarySerializer.cs | 12 +++++++----- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/AyCode.Core.Tests/TestModels/AcSerializerModels.cs b/AyCode.Core.Tests/TestModels/AcSerializerModels.cs index 68b9040..3c79ed8 100644 --- a/AyCode.Core.Tests/TestModels/AcSerializerModels.cs +++ b/AyCode.Core.Tests/TestModels/AcSerializerModels.cs @@ -1,4 +1,5 @@ using AyCode.Core.Interfaces; +using AyCode.Core.Serializers.Binaries; namespace AyCode.Core.Tests.TestModels; @@ -30,7 +31,9 @@ public static class AcSerializerModels public class TestClassWithRepeatedStrings { + [AcStringIntern(true)] public string Field1 { get; set; } = ""; + [AcStringIntern(true)] public string Field2 { get; set; } = ""; public string Field3 { get; set; } = ""; public string Field4 { get; set; } = ""; diff --git a/AyCode.Core.Tests/TestModels/SharedTestModels.cs b/AyCode.Core.Tests/TestModels/SharedTestModels.cs index 3f04e1c..2ab95a8 100644 --- a/AyCode.Core.Tests/TestModels/SharedTestModels.cs +++ b/AyCode.Core.Tests/TestModels/SharedTestModels.cs @@ -1,6 +1,7 @@ using AyCode.Core.Extensions; using AyCode.Core.Interfaces; using AyCode.Core.Serializers.Attributes; +using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Jsons; using MessagePack; using MongoDB.Bson.Serialization.Attributes; @@ -60,6 +61,7 @@ public class SharedTag : IId public int Id { get; set; } [Key(1)] public string Name { get; set; } = ""; + [AcStringIntern(true)] [Key(2)] public string Color { get; set; } = "#000000"; [Key(3)] @@ -133,12 +135,15 @@ public class SharedUser : IId [MessagePackObject] public class UserPreferences { + [AcStringIntern(true)] [Key(0)] public string Theme { get; set; } = "light"; + [AcStringIntern(true)] [Key(1)] public string Language { get; set; } = "en-US"; [Key(2)] public bool NotificationsEnabled { get; set; } = true; + [AcStringIntern(true)] [Key(3)] public string? EmailDigestFrequency { get; set; } } @@ -155,8 +160,10 @@ public class UserPreferences [MessagePackObject] public class MetadataInfo { + [AcStringIntern(true)] [Key(0)] public string Key { get; set; } = ""; + [AcStringIntern(true)] [Key(1)] public string Value { get; set; } = ""; [Key(2)] @@ -240,6 +247,7 @@ public class TestOrderItem : IId { [Key(0)] public int Id { get; set; } + [AcStringIntern(true)] [Key(1)] public string ProductName { get; set; } = ""; [Key(2)] diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index b4cf08f..16c155e 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -146,6 +146,13 @@ public static partial class AcBinarySerializer /// Write pass visit counter. Mirrors ScanVisitIndex ordering. internal int WriteVisitIndex; + /// + /// Set per-property in WritePropertyOrSkip before calling WriteString. + /// Controls whether the current string property participates in the cursor-based interning. + /// Must mirror scan pass's prop.UseStringPropertyInterning() check. + /// + internal bool StringInternEligible; + /// /// Tries to consume the next write plan entry at the current WriteVisitIndex. /// Returns true if the entry matches (duplicate exists at this visit point). @@ -264,6 +271,7 @@ public static partial class AcBinarySerializer ScanVisitIndex = 0; WritePlanCursor = 0; WriteVisitIndex = 0; + StringInternEligible = false; // Clear write plan string references to avoid GC pinning, keep array if small enough if (_writePlan != null) diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 66e42e1..5e02695 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -912,8 +912,10 @@ public static partial class AcBinarySerializer return; } - if (context.UseStringInterning && context.IsValidForInterningString(value.Length)) + if (context.StringInternEligible && context.IsValidForInterningString(value.Length)) { + context.StringInternEligible = false; + if (context.TryConsumeWritePlanEntry(out var planEntry)) { ValidateWritePlanString(in planEntry, value); @@ -1383,6 +1385,7 @@ public static partial class AcBinarySerializer } else { + context.StringInternEligible = prop.UseStringPropertyInterning(context.Options.UseStringInterning); WriteString(value, context); } return; @@ -1391,6 +1394,8 @@ public static partial class AcBinarySerializer { // Object type (collection, complex object, byte[], dictionary) // Use pre-cached wrapper, fallback to GetWrapper on miss/polymorphism + // Set interning eligibility for string collection elements + context.StringInternEligible = prop.IsStringCollectionProperty && prop.UseStringPropertyInterning(context.Options.UseStringInterning); var value = prop.GetValue(obj); // SKIP marker only for null (reference types) @@ -1736,10 +1741,7 @@ public static partial class AcBinarySerializer /// Uses the typed Id getter to extract the Id and compares against the scan pass IdentityMap. /// [Conditional("DEBUG")] - private static void ValidateWritePlanObject( - in WriteDuplicateEntry planEntry, - object value, - TypeMetadataWrapper wrapper) + private static void ValidateWritePlanObject(in WriteDuplicateEntry planEntry, object value, TypeMetadataWrapper wrapper) { var metadata = wrapper.Metadata; switch (metadata.IdAccessorType)