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.
This commit is contained in:
Loretta 2026-02-15 19:01:21 +01:00
parent 6f88306e54
commit e30efff56c
4 changed files with 26 additions and 5 deletions

View File

@ -1,4 +1,5 @@
using AyCode.Core.Interfaces; using AyCode.Core.Interfaces;
using AyCode.Core.Serializers.Binaries;
namespace AyCode.Core.Tests.TestModels; namespace AyCode.Core.Tests.TestModels;
@ -30,7 +31,9 @@ public static class AcSerializerModels
public class TestClassWithRepeatedStrings public class TestClassWithRepeatedStrings
{ {
[AcStringIntern(true)]
public string Field1 { get; set; } = ""; public string Field1 { get; set; } = "";
[AcStringIntern(true)]
public string Field2 { get; set; } = ""; public string Field2 { get; set; } = "";
public string Field3 { get; set; } = ""; public string Field3 { get; set; } = "";
public string Field4 { get; set; } = ""; public string Field4 { get; set; } = "";

View File

@ -1,6 +1,7 @@
using AyCode.Core.Extensions; using AyCode.Core.Extensions;
using AyCode.Core.Interfaces; using AyCode.Core.Interfaces;
using AyCode.Core.Serializers.Attributes; using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons; using AyCode.Core.Serializers.Jsons;
using MessagePack; using MessagePack;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
@ -60,6 +61,7 @@ public class SharedTag : IId<int>
public int Id { get; set; } public int Id { get; set; }
[Key(1)] [Key(1)]
public string Name { get; set; } = ""; public string Name { get; set; } = "";
[AcStringIntern(true)]
[Key(2)] [Key(2)]
public string Color { get; set; } = "#000000"; public string Color { get; set; } = "#000000";
[Key(3)] [Key(3)]
@ -133,12 +135,15 @@ public class SharedUser : IId<int>
[MessagePackObject] [MessagePackObject]
public class UserPreferences public class UserPreferences
{ {
[AcStringIntern(true)]
[Key(0)] [Key(0)]
public string Theme { get; set; } = "light"; public string Theme { get; set; } = "light";
[AcStringIntern(true)]
[Key(1)] [Key(1)]
public string Language { get; set; } = "en-US"; public string Language { get; set; } = "en-US";
[Key(2)] [Key(2)]
public bool NotificationsEnabled { get; set; } = true; public bool NotificationsEnabled { get; set; } = true;
[AcStringIntern(true)]
[Key(3)] [Key(3)]
public string? EmailDigestFrequency { get; set; } public string? EmailDigestFrequency { get; set; }
} }
@ -155,8 +160,10 @@ public class UserPreferences
[MessagePackObject] [MessagePackObject]
public class MetadataInfo public class MetadataInfo
{ {
[AcStringIntern(true)]
[Key(0)] [Key(0)]
public string Key { get; set; } = ""; public string Key { get; set; } = "";
[AcStringIntern(true)]
[Key(1)] [Key(1)]
public string Value { get; set; } = ""; public string Value { get; set; } = "";
[Key(2)] [Key(2)]
@ -240,6 +247,7 @@ public class TestOrderItem : IId<int>
{ {
[Key(0)] [Key(0)]
public int Id { get; set; } public int Id { get; set; }
[AcStringIntern(true)]
[Key(1)] [Key(1)]
public string ProductName { get; set; } = ""; public string ProductName { get; set; } = "";
[Key(2)] [Key(2)]

View File

@ -146,6 +146,13 @@ public static partial class AcBinarySerializer
/// <summary>Write pass visit counter. Mirrors ScanVisitIndex ordering.</summary> /// <summary>Write pass visit counter. Mirrors ScanVisitIndex ordering.</summary>
internal int WriteVisitIndex; internal int WriteVisitIndex;
/// <summary>
/// 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.
/// </summary>
internal bool StringInternEligible;
/// <summary> /// <summary>
/// Tries to consume the next write plan entry at the current WriteVisitIndex. /// Tries to consume the next write plan entry at the current WriteVisitIndex.
/// Returns true if the entry matches (duplicate exists at this visit point). /// Returns true if the entry matches (duplicate exists at this visit point).
@ -264,6 +271,7 @@ public static partial class AcBinarySerializer
ScanVisitIndex = 0; ScanVisitIndex = 0;
WritePlanCursor = 0; WritePlanCursor = 0;
WriteVisitIndex = 0; WriteVisitIndex = 0;
StringInternEligible = false;
// Clear write plan string references to avoid GC pinning, keep array if small enough // Clear write plan string references to avoid GC pinning, keep array if small enough
if (_writePlan != null) if (_writePlan != null)

View File

@ -912,8 +912,10 @@ public static partial class AcBinarySerializer
return; return;
} }
if (context.UseStringInterning && context.IsValidForInterningString(value.Length)) if (context.StringInternEligible && context.IsValidForInterningString(value.Length))
{ {
context.StringInternEligible = false;
if (context.TryConsumeWritePlanEntry(out var planEntry)) if (context.TryConsumeWritePlanEntry(out var planEntry))
{ {
ValidateWritePlanString(in planEntry, value); ValidateWritePlanString(in planEntry, value);
@ -1383,6 +1385,7 @@ public static partial class AcBinarySerializer
} }
else else
{ {
context.StringInternEligible = prop.UseStringPropertyInterning(context.Options.UseStringInterning);
WriteString(value, context); WriteString(value, context);
} }
return; return;
@ -1391,6 +1394,8 @@ public static partial class AcBinarySerializer
{ {
// Object type (collection, complex object, byte[], dictionary) // Object type (collection, complex object, byte[], dictionary)
// Use pre-cached wrapper, fallback to GetWrapper on miss/polymorphism // 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); var value = prop.GetValue(obj);
// SKIP marker only for null (reference types) // 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. /// Uses the typed Id getter to extract the Id and compares against the scan pass IdentityMap.
/// </summary> /// </summary>
[Conditional("DEBUG")] [Conditional("DEBUG")]
private static void ValidateWritePlanObject( private static void ValidateWritePlanObject(in WriteDuplicateEntry planEntry, object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper)
in WriteDuplicateEntry planEntry,
object value,
TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper)
{ {
var metadata = wrapper.Metadata; var metadata = wrapper.Metadata;
switch (metadata.IdAccessorType) switch (metadata.IdAccessorType)