Refactor: unify reference handling, footer string interning, benchmarks

- Replace UseReferenceHandling bool with ReferenceHandlingMode enum across all serializers and options
- Move AcBinary string interning to footer (no header shifting); remove bloom filter
- Overhaul benchmark console: multi-serializer, grouped/colorized results, CSV/log output, more test data
- Set UseMetadata = false by default (header property names unused)
- Update all context Reset/Pool logic for new options signature
- Update AcJson/Toon serializers for new reference handling
- Update tests and usages for new enum-based options
- Add Newtonsoft.Json to benchmark dependencies
- Misc: code cleanups, improved comments, clarify logic
This commit is contained in:
Loretta 2026-01-23 10:50:19 +01:00
parent 905b1c404d
commit cdf3cf34f8
23 changed files with 971 additions and 468 deletions

View File

@ -535,7 +535,7 @@ public abstract class AcBinaryOptionsBenchmarkBase
{
UseMetadata = false,
UseStringInterning = false,
UseReferenceHandling = false
ReferenceHandling = ReferenceHandlingMode.None,
},
_ => new AcBinarySerializerOptions()
};

View File

@ -7,6 +7,7 @@
<ItemGroup>
<PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<PropertyGroup>

View File

@ -1,80 +1,128 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Tests.TestModels;
using MessagePack;
using MessagePack.Resolvers;
using Newtonsoft.Json;
namespace AyCode.Core.Serializers.Console;
/// <summary>
/// Console application for Performance Diagnostics profiling.
/// Run with: Debug > Performance Profiler in Visual Studio
/// Comprehensive benchmark application for all serializers.
/// Compares: AcBinary (all options), AcJson, MessagePack, Newtonsoft.Json, System.Text.Json
///
/// Usage:
/// dotnet run -- serialize # Profile serialize only
/// dotnet run -- deserialize # Profile deserialize only
/// dotnet run -- all # Profile both (default)
/// dotnet run # Run all benchmarks
/// dotnet run -- quick # Quick mode (fewer iterations)
/// dotnet run -- serialize # Serialize only
/// dotnet run -- deserialize # Deserialize only
/// </summary>
public static class Program
{
private const int WarmupIterations = 50;
private const int TestIterations = 5000;
private const string ResultsDirectory = @"H:\Applications\Aycode\Source\AyCode.Core\Test_Benchmark_Results\Benchmark";
// 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!;
#if DEBUG
private const string BuildConfiguration = "Debug";
#else
private const string BuildConfiguration = "Release";
#endif
// Serializer name constants
private const string SerializerMessagePack = "MessagePack";
private const string SerializerAcBinaryDefault = "AcBinary (Default)";
private const string SerializerAcBinaryNoRef = "AcBinary (NoRef)";
private const string SerializerAcBinaryFastMode = "AcBinary (FastMode)";
private const string SerializerAcBinaryNoIntern = "AcBinary (NoIntern)";
private const string SerializerAcJsonDefault = "AcJson (Default)";
private const string SerializerNewtonsoftJson = "Newtonsoft.Json";
private const string SerializerSystemTextJson = "System.Text.Json";
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
private static int WarmupIterations = 10;
private static int TestIterations = 1000;
public static void Main(string[] args)
{
// Set console encoding to UTF-8 for proper Unicode character display
System.Console.OutputEncoding = System.Text.Encoding.UTF8;
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)
if (mode == "quick")
{
case "serialize":
case "ser":
RunSerializeTests();
break;
case "deserialize":
case "des":
RunDeserializeTests();
break;
default:
RunSerializeTests();
RunDeserializeTests();
break;
WarmupIterations = 10;
TestIterations = 100;
mode = "all";
}
sw.Stop();
System.Console.WriteLine($"\nTotal time: {sw.ElapsedMilliseconds:N0} ms");
System.Console.WriteLine("=".PadRight(60, '='));
System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════╗");
System.Console.WriteLine("║ COMPREHENSIVE SERIALIZER BENCHMARK SUITE ║");
System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════╝");
System.Console.WriteLine($"Mode: {mode} | Iterations: {TestIterations} | Warmup: {WarmupIterations}");
System.Console.WriteLine($"Build: {BuildConfiguration} | .NET: {Environment.Version}");
System.Console.WriteLine();
var allResults = new List<BenchmarkResult>();
var testDataSets = CreateTestDataSets();
foreach (var testData in testDataSets)
{
System.Console.WriteLine($"\n{'═'.ToString().PadRight(70, '═')}");
System.Console.WriteLine($"TEST DATA: {testData.Name}");
System.Console.WriteLine($"{'═'.ToString().PadRight(70, '═')}");
var results = RunBenchmarksForTestData(testData, mode);
allResults.AddRange(results);
}
private static void Setup()
// Print grouped results
PrintGroupedResults(allResults, testDataSets);
// Save results to file
SaveResults(allResults, testDataSets);
System.Console.WriteLine("\n✓ Benchmark complete!");
}
#region Test Data Creation
private static List<TestDataSet> CreateTestDataSets()
{
return new List<TestDataSet>
{
CreateSmallTestData(),
CreateMediumTestData(),
CreateLargeTestData(),
CreateRepeatedStringsTestData(),
CreateDeepNestedTestData()
};
}
private static TestDataSet CreateSmallTestData()
{
TestDataFactory.ResetIdCounter();
var order = TestDataFactory.CreateOrder(
itemCount: 2,
palletsPerItem: 2,
measurementsPerPallet: 2,
pointsPerMeasurement: 2);
return new TestDataSet("Small (2x2x2x2)", order);
}
private static TestDataSet CreateMediumTestData()
{
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(
var order = TestDataFactory.CreateOrder(
itemCount: 3,
palletsPerItem: 3,
measurementsPerPallet: 3,
@ -83,113 +131,632 @@ public static class Program
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");
return new TestDataSet("Medium (3x3x3x4, shared refs)", order);
}
private static void Warmup()
private static TestDataSet CreateLargeTestData()
{
TestDataFactory.ResetIdCounter();
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser");
var order = TestDataFactory.CreateOrder(
itemCount: 5,
palletsPerItem: 5,
measurementsPerPallet: 5,
pointsPerMeasurement: 10,
sharedTag: sharedTag,
sharedUser: sharedUser);
return new TestDataSet("Large (5x5x5x10)", order);
}
private static TestDataSet CreateRepeatedStringsTestData()
{
TestDataFactory.ResetIdCounter();
// 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);
// 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";
}
return new TestDataSet("Repeated Strings (10 items)", order);
}
private static TestDataSet CreateDeepNestedTestData()
{
TestDataFactory.ResetIdCounter();
var order = TestDataFactory.CreateOrder(
itemCount: 2,
palletsPerItem: 4,
measurementsPerPallet: 4,
pointsPerMeasurement: 8);
return new TestDataSet("Deep Nested (2x4x4x8)", order);
}
#endregion
#region Benchmark Execution
private static List<BenchmarkResult> RunBenchmarksForTestData(TestDataSet testData, string mode)
{
var results = new List<BenchmarkResult>();
var serializers = CreateSerializers(testData);
// Warmup all serializers
System.Console.WriteLine($"Warming up ({WarmupIterations} iterations)...");
for (int i = 0; i < WarmupIterations; i++)
foreach (var serializer in serializers)
{
DoSerializeAcBinary();
DoSerializeAcBinaryNoRef();
DoSerializeMsgPack();
DoDeserializeAcBinary();
DoDeserializeAcBinaryNoRef();
DoDeserializeMsgPack();
}
serializer.Warmup(WarmupIterations);
}
private static void RunSerializeTests()
{
System.Console.WriteLine("--- SERIALIZE ---");
// Run benchmarks
System.Console.WriteLine($"Running benchmarks ({TestIterations} iterations)...\n");
foreach (var serializer in serializers)
{
var result = new BenchmarkResult
{
TestDataName = testData.Name,
SerializerName = serializer.Name,
SerializedSize = serializer.SerializedSize
};
if (mode is "all" or "serialize" or "ser")
{
result.SerializeTimeMs = RunTimed(() => serializer.Serialize(), TestIterations);
}
if (mode is "all" or "deserialize" or "des")
{
result.DeserializeTimeMs = RunTimed(() => serializer.Deserialize(), TestIterations);
}
results.Add(result);
PrintResult(result);
}
return results;
}
private static List<ISerializerBenchmark> CreateSerializers(TestDataSet testData)
{
return new List<ISerializerBenchmark>
{
// AcBinary variants
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling(), SerializerAcBinaryNoRef),
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = false }, SerializerAcBinaryNoIntern),
// AcJson
new AcJsonBenchmark(testData.Order, AcJsonSerializerOptions.Default, SerializerAcJsonDefault),
// MessagePack
new MessagePackBenchmark(testData.Order, SerializerMessagePack),
// Newtonsoft.Json
new NewtonsoftBenchmark(testData.Order, SerializerNewtonsoftJson),
// System.Text.Json
new SystemTextJsonBenchmark(testData.Order, SerializerSystemTextJson)
};
}
private static double RunTimed(Action action, int iterations)
{
var sw = Stopwatch.StartNew();
for (int i = 0; i < TestIterations; i++)
for (var i = 0; i < iterations; i++)
{
DoSerializeAcBinary();
action();
}
sw.Stop();
System.Console.WriteLine($" AcBinary (WithRef): {sw.ElapsedMilliseconds,6:N0} ms");
return sw.Elapsed.TotalMilliseconds;
}
sw.Restart();
for (int i = 0; i < TestIterations; i++)
#endregion
#region Serializer Implementations
private interface ISerializerBenchmark
{
DoSerializeAcBinaryNoRef();
string Name { get; }
int SerializedSize { get; }
void Warmup(int iterations);
void Serialize();
void Deserialize();
}
sw.Stop();
System.Console.WriteLine($" AcBinary (NoRef): {sw.ElapsedMilliseconds,6:N0} ms");
sw.Restart();
for (int i = 0; i < TestIterations; i++)
private sealed class AcBinaryBenchmark : ISerializerBenchmark
{
DoSerializeMsgPack();
}
sw.Stop();
System.Console.WriteLine($" MessagePack: {sw.ElapsedMilliseconds,6:N0} ms");
}
private readonly TestOrder _order;
private readonly AcBinarySerializerOptions _options;
private readonly byte[] _serialized;
private static void RunDeserializeTests()
public string Name { get; }
public int SerializedSize => _serialized.Length;
public AcBinaryBenchmark(TestOrder order, AcBinarySerializerOptions options, string name)
{
System.Console.WriteLine("--- DESERIALIZE ---");
_order = order;
_options = options;
Name = name;
_serialized = AcBinarySerializer.Serialize(order, options);
}
var sw = Stopwatch.StartNew();
for (int i = 0; i < TestIterations; i++)
public void Warmup(int iterations)
{
DoDeserializeAcBinary();
}
sw.Stop();
System.Console.WriteLine($" AcBinary (WithRef): {sw.ElapsedMilliseconds,6:N0} ms");
sw.Restart();
for (int i = 0; i < TestIterations; i++)
for (var i = 0; i < iterations; i++)
{
DoDeserializeAcBinaryNoRef();
Serialize();
Deserialize();
}
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);
public void Serialize() => AcBinarySerializer.Serialize(_order, _options);
[MethodImpl(MethodImplOptions.NoInlining)]
private static byte[] DoSerializeMsgPack()
=> MessagePackSerializer.Serialize(s_testOrder, s_msgPackOptions);
public void Deserialize() => AcBinaryDeserializer.Deserialize<TestOrder>(_serialized, _options);
}
private sealed class AcJsonBenchmark : ISerializerBenchmark
{
private readonly TestOrder _order;
private readonly AcJsonSerializerOptions _options;
private readonly string _serialized;
private readonly byte[] _serializedUtf8;
public string Name { get; }
public int SerializedSize => _serializedUtf8.Length;
public AcJsonBenchmark(TestOrder order, AcJsonSerializerOptions options, string name)
{
_order = order;
_options = options;
Name = name;
_serialized = AcJsonSerializer.Serialize(order, options);
_serializedUtf8 = Utf8NoBom.GetBytes(_serialized);
}
public void Warmup(int iterations)
{
for (var i = 0; i < iterations; i++)
{
Serialize();
Deserialize();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static TestOrder? DoDeserializeAcBinary()
=> AcBinaryDeserializer.Deserialize<TestOrder>(s_acBinaryData);
public void Serialize() => AcJsonSerializer.Serialize(_order, _options);
[MethodImpl(MethodImplOptions.NoInlining)]
private static TestOrder? DoDeserializeAcBinaryNoRef()
=> AcBinaryDeserializer.Deserialize<TestOrder>(s_acBinaryNoRefData);
public void Deserialize() => AcJsonDeserializer.Deserialize<TestOrder>(_serialized);
}
private sealed class MessagePackBenchmark : ISerializerBenchmark
{
private readonly TestOrder _order;
private readonly MessagePackSerializerOptions _options;
private readonly byte[] _serialized;
public string Name { get; }
public int SerializedSize => _serialized.Length;
public MessagePackBenchmark(TestOrder order, string name)
{
_order = order;
Name = name;
_options = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
_serialized = MessagePackSerializer.Serialize(order, _options);
}
public void Warmup(int iterations)
{
for (var i = 0; i < iterations; i++)
{
Serialize();
Deserialize();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static TestOrder? DoDeserializeMsgPack()
=> MessagePackSerializer.Deserialize<TestOrder>(s_msgPackData, s_msgPackOptions);
public void Serialize() => MessagePackSerializer.Serialize(_order, _options);
[MethodImpl(MethodImplOptions.NoInlining)]
public void Deserialize() => MessagePackSerializer.Deserialize<TestOrder>(_serialized, _options);
}
private sealed class NewtonsoftBenchmark : ISerializerBenchmark
{
private readonly TestOrder _order;
private readonly JsonSerializerSettings _settings;
private readonly string _serialized;
private readonly byte[] _serializedUtf8;
public string Name { get; }
public int SerializedSize => _serializedUtf8.Length;
public NewtonsoftBenchmark(TestOrder order, string name)
{
_order = order;
Name = name;
_settings = new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore
};
_serialized = JsonConvert.SerializeObject(order, _settings);
_serializedUtf8 = Utf8NoBom.GetBytes(_serialized);
}
public void Warmup(int iterations)
{
for (var i = 0; i < iterations; i++)
{
Serialize();
Deserialize();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public void Serialize() => JsonConvert.SerializeObject(_order, _settings);
[MethodImpl(MethodImplOptions.NoInlining)]
public void Deserialize() => JsonConvert.DeserializeObject<TestOrder>(_serialized, _settings);
}
private sealed class SystemTextJsonBenchmark : ISerializerBenchmark
{
private readonly TestOrder _order;
private readonly JsonSerializerOptions _options;
private readonly string _serialized;
private readonly byte[] _serializedUtf8;
public string Name { get; }
public int SerializedSize => _serializedUtf8.Length;
public SystemTextJsonBenchmark(TestOrder order, string name)
{
_order = order;
Name = name;
_options = new JsonSerializerOptions
{
WriteIndented = false,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles
};
_serialized = System.Text.Json.JsonSerializer.Serialize(order, _options);
_serializedUtf8 = Utf8NoBom.GetBytes(_serialized);
}
public void Warmup(int iterations)
{
for (var i = 0; i < iterations; i++)
{
Serialize();
Deserialize();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public void Serialize() => System.Text.Json.JsonSerializer.Serialize(_order, _options);
[MethodImpl(MethodImplOptions.NoInlining)]
public void Deserialize() => System.Text.Json.JsonSerializer.Deserialize<TestOrder>(_serialized, _options);
}
#endregion
#region Results
private sealed class TestDataSet
{
public string Name { get; }
public TestOrder Order { get; }
public TestDataSet(string name, TestOrder order)
{
Name = name;
Order = order;
}
}
private sealed class BenchmarkResult
{
public string TestDataName { get; set; } = "";
public string SerializerName { get; set; } = "";
public int SerializedSize { get; set; }
public double SerializeTimeMs { get; set; }
public double DeserializeTimeMs { get; set; }
public double RoundTripTimeMs => SerializeTimeMs + DeserializeTimeMs;
}
private static void PrintResult(BenchmarkResult result)
{
var ser = result.SerializeTimeMs > 0 ? $"{result.SerializeTimeMs,8:F2} ms" : " N/A";
var des = result.DeserializeTimeMs > 0 ? $"{result.DeserializeTimeMs,8:F2} ms" : " N/A";
System.Console.WriteLine($" {result.SerializerName,-25} | Size: {result.SerializedSize,8:N0} | Ser: {ser} | Des: {des}");
}
private static void PrintGroupedResults(List<BenchmarkResult> results, List<TestDataSet> testDataSets)
{
System.Console.WriteLine("\n");
System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗");
System.Console.WriteLine("║ GROUPED RESULTS BY TEST DATA ║");
System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
foreach (var testData in testDataSets)
{
var testResults = results.Where(r => r.TestDataName == testData.Name).OrderBy(r => r.RoundTripTimeMs).ToList();
var msgPackResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerMessagePack);
var acBinaryResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerAcBinaryDefault);
System.Console.WriteLine($"\n┌─ {testData.Name} ─".PadRight(98, '─') + "┐");
System.Console.WriteLine($"│ {"#",-4} │ {"Serializer",-25} │ {"Size",-10} │ {"Serialize",-12} │ {"Deserialize",-12} │ {"Round-trip",-12} │");
System.Console.WriteLine($"├{"".PadRight(6, '─')}┼{"".PadRight(27, '─')}┼{"".PadRight(12, '─')}┼{"".PadRight(14, '─')}┼{"".PadRight(14, '─')}┼{"".PadRight(14, '─')}┤");
var rank = 1;
foreach (var result in testResults)
{
var size = $"{result.SerializedSize:N0}";
var ser = result.SerializeTimeMs > 0 ? $"{result.SerializeTimeMs:F2} ms" : "N/A";
var des = result.DeserializeTimeMs > 0 ? $"{result.DeserializeTimeMs:F2} ms" : "N/A";
var rt = result.RoundTripTimeMs > 0 ? $"{result.RoundTripTimeMs:F2} ms" : "N/A";
// Highlight MessagePack and AcBinary (Default) with win/lose colors
var isHighlighted = result.SerializerName is SerializerMessagePack or SerializerAcBinaryDefault;
var prefix = isHighlighted ? "│►" : "│ ";
var suffix = isHighlighted ? "◄│" : " │";
// Color logic: Green = winner (faster), Red = loser (slower)
if (isHighlighted && msgPackResult != null && acBinaryResult != null)
{
var isMsgPack = result.SerializerName == SerializerMessagePack;
var msgPackFaster = msgPackResult.RoundTripTimeMs < acBinaryResult.RoundTripTimeMs;
if (isMsgPack)
{
System.Console.ForegroundColor = msgPackFaster ? ConsoleColor.Green : ConsoleColor.Red;
}
else
{
System.Console.ForegroundColor = msgPackFaster ? ConsoleColor.Red : ConsoleColor.Green;
}
}
System.Console.WriteLine($"{prefix}{rank++,4} │ {result.SerializerName,-25} │ {size,10} │ {ser,12} │ {des,12} │ {rt,12}{suffix}");
if (isHighlighted)
{
System.Console.ResetColor();
}
}
// Footer row: AcBinary (Default) vs MessagePack comparison per column
if (msgPackResult != null && acBinaryResult != null)
{
var sizePct = (acBinaryResult.SerializedSize / (double)msgPackResult.SerializedSize - 1) * 100;
var serPct = msgPackResult.SerializeTimeMs > 0 ? (acBinaryResult.SerializeTimeMs / msgPackResult.SerializeTimeMs - 1) * 100 : 0;
var desPct = msgPackResult.DeserializeTimeMs > 0 ? (acBinaryResult.DeserializeTimeMs / msgPackResult.DeserializeTimeMs - 1) * 100 : 0;
var rtPct = msgPackResult.RoundTripTimeMs > 0 ? (acBinaryResult.RoundTripTimeMs / msgPackResult.RoundTripTimeMs - 1) * 100 : 0;
System.Console.WriteLine($"├{"".PadRight(6, '─')}┴{"".PadRight(27, '─')}┼{"".PadRight(12, '─')}┼{"".PadRight(14, '─')}┼{"".PadRight(14, '─')}┼{"".PadRight(13, '─')}┤");
System.Console.Write($"│ ► Default vs {SerializerMessagePack,-19} │ ");
// Size
System.Console.ForegroundColor = sizePct <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
System.Console.Write($"{sizePct,+9:+0;-0}%");
System.Console.ResetColor();
System.Console.Write(" │ ");
// Serialize
System.Console.ForegroundColor = serPct <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
System.Console.Write($"{serPct,+11:+0;-0}%");
System.Console.ResetColor();
System.Console.Write(" │ ");
// Deserialize
System.Console.ForegroundColor = desPct <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
System.Console.Write($"{desPct,+11:+0;-0}%");
System.Console.ResetColor();
System.Console.Write(" │ ");
// Round-trip
System.Console.ForegroundColor = rtPct <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
System.Console.Write($"{rtPct,+10:+0;-0}%");
System.Console.ResetColor();
System.Console.WriteLine(" │");
}
System.Console.WriteLine($"└{"".PadRight(6, '─')}─{"".PadRight(27, '─')}┴{"".PadRight(12, '─')}┴{"".PadRight(14, '─')}┴{"".PadRight(14, '─')}┴{"".PadRight(13, '─')}┘");
}
// Summary: Best serializer for each category
System.Console.WriteLine("\n");
System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗");
System.Console.WriteLine("║ SUMMARY: WINNERS ║");
System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
System.Console.WriteLine($"\n{"Category",-20} │ {"Winner",-25} │ {"Avg Value",-18}");
System.Console.WriteLine($"{"".PadRight(20, '─')}─┼─{"".PadRight(25, '─')}─┼─{"".PadRight(18, '─')}");
// Fastest Serialize
var fastestSer = results.Where(r => r.SerializeTimeMs > 0)
.GroupBy(r => r.SerializerName)
.Select(g => new { Name = g.Key, AvgTime = g.Average(r => r.SerializeTimeMs) })
.OrderBy(x => x.AvgTime)
.FirstOrDefault();
if (fastestSer != null)
System.Console.WriteLine($"{"Fastest Serialize",-20} │ {fastestSer.Name,-25} │ {fastestSer.AvgTime,15:F2} ms");
// Fastest Deserialize
var fastestDes = results.Where(r => r.DeserializeTimeMs > 0)
.GroupBy(r => r.SerializerName)
.Select(g => new { Name = g.Key, AvgTime = g.Average(r => r.DeserializeTimeMs) })
.OrderBy(x => x.AvgTime)
.FirstOrDefault();
if (fastestDes != null)
System.Console.WriteLine($"{"Fastest Deserialize",-20} │ {fastestDes.Name,-25} │ {fastestDes.AvgTime,15:F2} ms");
// Smallest Size
var smallestSize = results
.GroupBy(r => r.SerializerName)
.Select(g => new { Name = g.Key, AvgSize = g.Average(r => r.SerializedSize) })
.OrderBy(x => x.AvgSize)
.FirstOrDefault();
if (smallestSize != null)
System.Console.WriteLine($"{"Smallest Size",-20} │ {smallestSize.Name,-25} │ {smallestSize.AvgSize,15:F0} B");
// Fastest Round-trip
var fastestRt = results.Where(r => r.RoundTripTimeMs > 0)
.GroupBy(r => r.SerializerName)
.Select(g => new { Name = g.Key, AvgTime = g.Average(r => r.RoundTripTimeMs) })
.OrderBy(x => x.AvgTime)
.FirstOrDefault();
if (fastestRt != null)
System.Console.WriteLine($"{"Fastest Round-trip",-20} │ {fastestRt.Name,-25} │ {fastestRt.AvgTime,15:F2} ms");
// Overall AcBinary Default vs MessagePack comparison
var msgPackAvgSer = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).Average(r => r.SerializeTimeMs);
var msgPackAvgDes = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs);
var msgPackAvgRt = results.Where(r => r.SerializerName == SerializerMessagePack && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs);
var msgPackAvgSize = results.Where(r => r.SerializerName == SerializerMessagePack).Average(r => r.SerializedSize);
var acBinaryAvgSer = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.SerializeTimeMs > 0).Average(r => r.SerializeTimeMs);
var acBinaryAvgDes = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs);
var acBinaryAvgRt = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs);
var acBinaryAvgSize = results.Where(r => r.SerializerName == SerializerAcBinaryDefault).Average(r => r.SerializedSize);
System.Console.WriteLine();
System.Console.WriteLine($"── {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ──");
var serPctAll = (acBinaryAvgSer / msgPackAvgSer - 1) * 100;
var desPctAll = (acBinaryAvgDes / msgPackAvgDes - 1) * 100;
var rtPctAll = (acBinaryAvgRt / msgPackAvgRt - 1) * 100;
var sizePctAll = (acBinaryAvgSize / msgPackAvgSize - 1) * 100;
System.Console.ForegroundColor = serPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
System.Console.WriteLine($" Serialize: {serPctAll:+0;-0}% ({acBinaryAvgSer:F2} ms vs {msgPackAvgSer:F2} ms)");
System.Console.ResetColor();
System.Console.ForegroundColor = desPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
System.Console.WriteLine($" Deserialize: {desPctAll:+0;-0}% ({acBinaryAvgDes:F2} ms vs {msgPackAvgDes:F2} ms)");
System.Console.ResetColor();
System.Console.ForegroundColor = rtPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
System.Console.WriteLine($" Round-trip: {rtPctAll:+0;-0}% ({acBinaryAvgRt:F2} ms vs {msgPackAvgRt:F2} ms)");
System.Console.ResetColor();
System.Console.ForegroundColor = sizePctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
System.Console.WriteLine($" Size: {sizePctAll:+0;-0}% ({acBinaryAvgSize:F0} B vs {msgPackAvgSize:F0} B)");
System.Console.ResetColor();
}
private static void SaveResults(List<BenchmarkResult> results, List<TestDataSet> testDataSets)
{
Directory.CreateDirectory(ResultsDirectory);
var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
var fileName = $"Console.FullBenchmark_{BuildConfiguration}_{timestamp}.log";
var filePath = Path.Combine(ResultsDirectory, fileName);
var sb = new StringBuilder();
sb.AppendLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗");
sb.AppendLine("║ SERIALIZER BENCHMARK RESULTS ║");
sb.AppendLine($"║ Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}".PadRight(100) + "║");
sb.AppendLine($"║ Build: {BuildConfiguration}".PadRight(100) + "║");
sb.AppendLine($"║ Iterations: {TestIterations}".PadRight(100) + "║");
sb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
sb.AppendLine();
// CSV-like data for easy import
sb.AppendLine("=== RAW DATA (CSV) ===");
sb.AppendLine("TestData,Serializer,Size,SerializeMs,DeserializeMs,RoundTripMs");
foreach (var testData in testDataSets)
{
var testResults = results.Where(r => r.TestDataName == testData.Name).ToList();
foreach (var result in testResults)
{
sb.AppendLine($"{result.TestDataName},{result.SerializerName},{result.SerializedSize},{result.SerializeTimeMs:F2},{result.DeserializeTimeMs:F2},{result.RoundTripTimeMs:F2}");
}
}
sb.AppendLine();
// Formatted results
sb.AppendLine("=== FORMATTED RESULTS BY TEST DATA ===");
sb.AppendLine($"(►) = Highlighted: {SerializerMessagePack} (baseline) and {SerializerAcBinaryDefault}");
sb.AppendLine();
foreach (var testData in testDataSets)
{
var testResults = results.Where(r => r.TestDataName == testData.Name).OrderBy(r => r.RoundTripTimeMs).ToList();
var msgPackResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerMessagePack);
var acBinaryResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerAcBinaryDefault);
sb.AppendLine();
sb.AppendLine($"--- {testData.Name} ---");
sb.AppendLine($"{"#",-4} {"Serializer",-26} {"Size",-12} {"Serialize",-14} {"Deserialize",-14} {"Round-trip",-14}");
sb.AppendLine(new string('-', 86));
var rank = 1;
foreach (var result in testResults)
{
var isHighlighted = result.SerializerName is SerializerMessagePack or SerializerAcBinaryDefault;
var prefix = isHighlighted ? "► " : " ";
var size = $"{result.SerializedSize:N0}";
var ser = result.SerializeTimeMs > 0 ? $"{result.SerializeTimeMs:F2} ms" : "N/A";
var des = result.DeserializeTimeMs > 0 ? $"{result.DeserializeTimeMs:F2} ms" : "N/A";
var rt = result.RoundTripTimeMs > 0 ? $"{result.RoundTripTimeMs:F2} ms" : "N/A";
sb.AppendLine($"{rank++,2} {prefix}{result.SerializerName,-24} {size,-12} {ser,-14} {des,-14} {rt,-14}");
}
// Summary row for this test data
if (msgPackResult != null && acBinaryResult != null)
{
var sizePct = (acBinaryResult.SerializedSize / (double)msgPackResult.SerializedSize - 1) * 100;
var serPct = msgPackResult.SerializeTimeMs > 0 ? (acBinaryResult.SerializeTimeMs / msgPackResult.SerializeTimeMs - 1) * 100 : 0;
var desPct = msgPackResult.DeserializeTimeMs > 0 ? (acBinaryResult.DeserializeTimeMs / msgPackResult.DeserializeTimeMs - 1) * 100 : 0;
var rtPct = msgPackResult.RoundTripTimeMs > 0 ? (acBinaryResult.RoundTripTimeMs / msgPackResult.RoundTripTimeMs - 1) * 100 : 0;
sb.AppendLine($" {SerializerAcBinaryDefault} vs {SerializerMessagePack}: Size {sizePct:+0;-0}% │ Ser {serPct:+0;-0}% │ Des {desPct:+0;-0}% │ RT {rtPct:+0;-0}%");
}
}
// Summary comparison
sb.AppendLine();
sb.AppendLine($"=== {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ===");
var msgPackAvgSer = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).Average(r => r.SerializeTimeMs);
var msgPackAvgDes = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs);
var msgPackAvgRt = results.Where(r => r.SerializerName == SerializerMessagePack && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs);
var msgPackAvgSize = results.Where(r => r.SerializerName == SerializerMessagePack).Average(r => r.SerializedSize);
var acBinaryAvgSer = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.SerializeTimeMs > 0).Average(r => r.SerializeTimeMs);
var acBinaryAvgDes = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs);
var acBinaryAvgRt = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs);
var acBinaryAvgSize = results.Where(r => r.SerializerName == SerializerAcBinaryDefault).Average(r => r.SerializedSize);
sb.AppendLine($" Serialize: {((acBinaryAvgSer / msgPackAvgSer - 1) * 100):+0;-0}% ({acBinaryAvgSer:F2} ms vs {msgPackAvgSer:F2} ms)");
sb.AppendLine($" Deserialize: {((acBinaryAvgDes / msgPackAvgDes - 1) * 100):+0;-0}% ({acBinaryAvgDes:F2} ms vs {msgPackAvgDes:F2} ms)");
sb.AppendLine($" Round-trip: {((acBinaryAvgRt / msgPackAvgRt - 1) * 100):+0;-0}% ({acBinaryAvgRt:F2} ms vs {msgPackAvgRt:F2} ms)");
sb.AppendLine($" Size: {((acBinaryAvgSize / msgPackAvgSize - 1) * 100):+0;-0}% ({acBinaryAvgSize:F0} B vs {msgPackAvgSize:F0} B)");
File.WriteAllText(filePath, sb.ToString(), Utf8NoBom);
System.Console.WriteLine($"\n✓ Results saved to: {filePath}");
}
#endregion
}

View File

@ -364,7 +364,7 @@ public class AcBinarySerializerNavigationPropertyTests
Assert.AreEqual("SharedProduct", result.Items[1].Product.Name);
// With reference handling, they should be the same instance
// (This depends on UseReferenceHandling being enabled)
// (This depends on ReferenceHandling being enabled)
Console.WriteLine($"Same instance: {ReferenceEquals(result.Items[0].Product, result.Items[1].Product)}");
}
}

View File

@ -781,7 +781,7 @@ public static class AcSerializerCommon
/// <summary>
/// Common reference tracking for serialization.
/// Uses unified Bloom filter + HashSet for both IId and Reference tracking.
/// Uses HashSet for tracking seen objects.
/// - IId objects: hash = Id (positive, DB guarantees uniqueness per entity type)
/// - Non-IId objects: hash = RuntimeHelpers.GetHashCode | 0x80000000 (negative)
/// </summary>
@ -789,9 +789,6 @@ public static class AcSerializerCommon
{
private const int InitialCapacity = 128;
// Unified Bloom filter (256 bits = 4 x 64-bit)
private ulong _bloom0, _bloom1, _bloom2, _bloom3;
// Unified HashSet for seen hashes (both IId and Reference)
private HashSet<int>? _seenHashes;
@ -809,7 +806,6 @@ public static class AcSerializerCommon
public void Reset()
{
_nextRefId = 1;
_bloom0 = _bloom1 = _bloom2 = _bloom3 = 0;
_seenHashes?.Clear();
_multiRefHashes?.Clear();
_writtenRefs?.Clear();
@ -878,41 +874,14 @@ public static class AcSerializerCommon
}
/// <summary>
/// Core tracking logic using Bloom filter + HashSet.
/// Core tracking logic using HashSet.Add() which returns false if already exists.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TrackHash(int hash)
{
// Bloom filter check - fast "definitely new" detection
var segment = (hash >> 6) & 3;
var bit = hash & 63;
var mask = 1UL << bit;
var bloomHit = segment switch
{
0 => (_bloom0 & mask) != 0,
1 => (_bloom1 & mask) != 0,
2 => (_bloom2 & mask) != 0,
_ => (_bloom3 & mask) != 0
};
if (!bloomHit)
{
// Definitely new - add to bloom and set
switch (segment)
{
case 0: _bloom0 |= mask; break;
case 1: _bloom1 |= mask; break;
case 2: _bloom2 |= mask; break;
default: _bloom3 |= mask; break;
}
_seenHashes ??= new HashSet<int>(InitialCapacity);
_seenHashes.Add(hash);
return true;
}
// Possible duplicate - check HashSet
_seenHashes ??= new HashSet<int>(InitialCapacity);
// HashSet.Add returns false if element already exists
if (!_seenHashes.Add(hash))
{
// Already seen - multi-referenced!

View File

@ -1,3 +1,4 @@
using AyCode.Core.Serializers.Jsons;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@ -14,6 +15,8 @@ namespace AyCode.Core.Serializers;
/// <typeparam name="TMetadata">The concrete metadata type.</typeparam>
public abstract class AcSerializerContextBase<TMetadata> where TMetadata : TypeMetadataBase
{
public byte MaxDepth { get; private set; }
public ReferenceHandlingMode ReferenceHandling { get; internal set; }
/// <summary>
/// Global shared cache for metadata (thread-safe, shared across all contexts).
/// Generic specialization ensures separate cache per TMetadata type.
@ -116,12 +119,17 @@ public abstract class AcSerializerContextBase<TMetadata> where TMetadata : TypeM
/// Resets all wrapper tracking states for reuse.
/// Does not remove wrappers - keeps them for next operation.
/// </summary>
public virtual void Reset()
public virtual void Reset(in AcSerializerOptions? options)
{
foreach (var wrapper in _wrappers.Values)
{
wrapper.ResetTracking();
}
if (options == null) return;
MaxDepth = options.MaxDepth;
ReferenceHandling = options.ReferenceHandling;
}
#endregion

View File

@ -1,4 +1,5 @@
using System;
using AyCode.Core.Serializers.Jsons;
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
@ -32,7 +33,10 @@ public static partial class AcBinaryDeserializer
public readonly BinaryDeserializationContextClass ContextClass;
public bool HasMetadata { get; private set; }
public bool HasReferenceHandling { get; private set; }
/// <summary>
/// Convenience property - true if any reference handling is enabled.
/// </summary>
public bool HasReferenceHandling => ContextClass?.ReferenceHandling != ReferenceHandlingMode.None;
public bool IsMergeMode { readonly get; set; }
public bool RemoveOrphanedItems { readonly get; set; }
public bool IsAtEnd => _position >= _buffer.Length;
@ -69,7 +73,6 @@ public static partial class AcBinaryDeserializer
//_objectReferences = null;
_stringCache = null;
HasMetadata = false;
HasReferenceHandling = false;
IsMergeMode = false;
RemoveOrphanedItems = false;
ChainTracker = null;
@ -77,6 +80,8 @@ public static partial class AcBinaryDeserializer
_useStringCaching = options.UseStringCaching;
_maxCachedStringLength = options.MaxCachedStringLength;
ContextClass = contextClass;
// Initialize ReferenceHandling from options (will be overwritten by ReadHeader if present in stream)
ContextClass.ReferenceHandling = options.ReferenceHandling;
}
public void ReadHeader()
@ -97,22 +102,38 @@ public static partial class AcBinaryDeserializer
var marker = ReadByteInternal();
var hasPropertyTable = false;
var hasInternTable = false;
var hasInternFooter = false;
var footerPosition = 0;
if (marker == BinaryTypeCode.MetadataHeader)
{
hasPropertyTable = true;
HasReferenceHandling = true;
ContextClass.ReferenceHandling = ReferenceHandlingMode.OnlyId; // Legacy: assume OnlyId
}
else if (marker == BinaryTypeCode.NoMetadataHeader)
{
HasReferenceHandling = true;
ContextClass.ReferenceHandling = ReferenceHandlingMode.OnlyId; // Legacy: assume OnlyId
}
else if ((marker & 0xF0) == BinaryTypeCode.HeaderFlagsBase)
{
var flags = (byte)(marker & 0x0F);
hasPropertyTable = (flags & BinaryTypeCode.HeaderFlag_Metadata) != 0;
HasReferenceHandling = (flags & BinaryTypeCode.HeaderFlag_ReferenceHandling) != 0;
hasInternTable = (flags & BinaryTypeCode.HeaderFlag_StringInternTable) != 0;
// Decode ReferenceHandlingMode from separate bits
var hasOnlyId = (flags & BinaryTypeCode.HeaderFlag_RefHandling_OnlyId) != 0;
var hasAll = (flags & BinaryTypeCode.HeaderFlag_RefHandling_All) != 0;
ContextClass.ReferenceHandling = hasAll ? ReferenceHandlingMode.All
: hasOnlyId ? ReferenceHandlingMode.OnlyId
: ReferenceHandlingMode.None;
// Read footer position if flag is set
var hasFooterPosition = (flags & BinaryTypeCode.HeaderFlag_HasFooterPosition) != 0;
if (hasFooterPosition)
{
EnsureAvailable(4);
footerPosition = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position, 4));
_position += 4;
hasInternFooter = footerPosition > 0;
}
}
else
{
@ -133,6 +154,7 @@ public static partial class AcBinaryDeserializer
}
}
// Legacy: interned strings in header
if (hasInternTable)
{
var internCount = (int)ReadVarUInt();
@ -142,6 +164,36 @@ public static partial class AcBinaryDeserializer
_internedStrings.Add(ReadHeaderString());
}
}
// Footer-based: read interned strings from footer, then return to data position
if (hasInternFooter && footerPosition > 0)
{
ReadFooterStrings(footerPosition);
}
}
/// <summary>
/// Reads interned strings from footer position, then returns to data position.
/// Uses seek to footer, read strings, seek back to data.
/// </summary>
private void ReadFooterStrings(int footerPosition)
{
// Save current position (start of data)
var dataPosition = _position;
// Seek to footer
_position = footerPosition;
// Read interned strings
var internCount = (int)ReadVarUInt();
_internedStrings = new List<string>(internCount);
for (var i = 0; i < internCount; i++)
{
_internedStrings.Add(ReadHeaderString());
}
// Seek back to data position
_position = dataPosition;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@ -1,3 +1,4 @@
using AyCode.Core.Serializers.Jsons;
using System;
using System.Buffers;
using System.Collections.Concurrent;
@ -56,10 +57,6 @@ public static partial class AcBinarySerializer
private const int InitialInternCapacity = 32;
private const int InitialPropertyNameCapacity = 32;
// Bloom filter constants for string interning
private const int BloomFilterSize = 256; // 256 bits = 32 bytes
private const int BloomFilterMask = BloomFilterSize - 1;
private byte[] _buffer;
private int _position;
private int _initialBufferSize;
@ -70,24 +67,13 @@ public static partial class AcBinarySerializer
private Dictionary<string, int>? _internedStrings;
private List<string>? _internedStringList;
/// <summary>
/// Bloom filter for quick "definitely not interned" checks.
/// Avoids dictionary lookup for unique strings.
/// </summary>
private ulong _bloomFilter0;
private ulong _bloomFilter1;
private ulong _bloomFilter2;
private ulong _bloomFilter3;
private Dictionary<string, int>? _propertyNames;
private List<string>? _propertyNameList;
private int[]? _propertyIndexBuffer;
private byte[]? _propertyStateBuffer;
public bool UseReferenceHandling { get; private set; }
public bool UseStringInterning { get; private set; }
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; }
@ -109,25 +95,17 @@ public static partial class AcBinarySerializer
public void Reset(AcBinarySerializerOptions options)
{
// Reset wrapper tracking state from base class (IId tracking)
base.Reset(options);
_position = 0;
UseReferenceHandling = options.UseReferenceHandling;
UseStringInterning = options.UseStringInterning;
UseMetadata = options.UseMetadata;
MaxDepth = options.MaxDepth;
MinStringInternLength = options.MinStringInternLength;
MaxStringInternLength = options.MaxStringInternLength;
PropertyFilter = options.PropertyFilter;
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
//_refTracker.Reset();
if (UseReferenceHandling)
{
//_refTracker.EnsureInitialized();
}
// Reset wrapper tracking state from base class (IId tracking)
base.Reset();
if (_buffer.Length < _initialBufferSize)
{
ArrayPool<byte>.Shared.Return(_buffer);
@ -139,12 +117,6 @@ public static partial class AcBinarySerializer
{
_position = 0;
// Reset bloom filter
_bloomFilter0 = 0;
_bloomFilter1 = 0;
_bloomFilter2 = 0;
_bloomFilter3 = 0;
//_refTracker.Reset();
ClearAndTrimIfNeeded(_internedStrings, InitialInternCapacity * 4);
ClearAndTrimIfNeeded(_propertyNames, InitialPropertyNameCapacity * 4);
@ -169,7 +141,7 @@ public static partial class AcBinarySerializer
}
}
private void ResetCachedPropertyIndices()
private static void ResetCachedPropertyIndices()
{
// Note: BinaryPropertyAccessor.CachedPropertyNameIndex is per-context,
// but metadata is cached globally. We reset it during Clear to avoid
@ -205,6 +177,10 @@ public static partial class AcBinarySerializer
/// </summary>
private List<byte[]>? _internedStringUtf8;
/// <summary>
/// Registers a string for interning. Returns the index of the string.
/// Uses CollectionsMarshal.GetValueRefOrAddDefault for single-operation lookup+add.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int RegisterInternedString(string value)
{
@ -212,32 +188,17 @@ public static partial class AcBinarySerializer
_internedStringList ??= new List<string>(InitialInternCapacity);
_internedStringUtf8 ??= new List<byte[]>(InitialInternCapacity);
// Fast path: check bloom filter first
var hash = GetStringHash(value);
if (!BloomFilterMightContain(hash))
{
// Definitely not in dictionary - add directly
var newIndex = _internedStringList.Count;
_internedStrings[value] = newIndex;
_internedStringList.Add(value);
// Cache UTF8 bytes immediately
_internedStringUtf8.Add(GetUtf8BytesCached(value));
BloomFilterAdd(hash);
return newIndex;
}
// Might be in dictionary - need to check
// Single operation: lookup + conditional add
ref var index = ref CollectionsMarshal.GetValueRefOrAddDefault(_internedStrings, value, out var exists);
if (exists)
{
return index;
}
// New string - add to lists
index = _internedStringList.Count;
_internedStringList.Add(value);
// Cache UTF8 bytes immediately
_internedStringUtf8.Add(GetUtf8BytesCached(value));
BloomFilterAdd(hash);
return index;
}
@ -248,77 +209,16 @@ public static partial class AcBinarySerializer
private static byte[] GetUtf8BytesCached(string value)
{
// Fast path for ASCII strings - direct char to byte conversion
if (System.Text.Ascii.IsValid(value))
if (Ascii.IsValid(value))
{
var bytes = new byte[value.Length];
System.Text.Ascii.FromUtf16(value.AsSpan(), bytes, out _);
Ascii.FromUtf16(value.AsSpan(), bytes, out _);
return bytes;
}
// Standard path for multi-byte UTF8
return Utf8NoBom.GetBytes(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetStringHash(string value)
{
// Simple hash combining length and first/last characters
// Optimized for quick calculation, not collision resistance
if (value.Length == 0) return 0;
var h = value.Length;
h = (h * 31) + value[0];
if (value.Length > 1)
h = (h * 31) + value[value.Length - 1];
return h;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool BloomFilterMightContain(int hash)
{
// Use two hash functions for bloom filter
var h1 = hash & BloomFilterMask;
var h2 = (hash >> 8) & BloomFilterMask;
return BloomFilterTestBit(h1) && BloomFilterTestBit(h2);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool BloomFilterTestBit(int bit)
{
var segment = bit >> 6; // Divide by 64
var mask = 1UL << (bit & 63);
return segment switch
{
0 => (_bloomFilter0 & mask) != 0,
1 => (_bloomFilter1 & mask) != 0,
2 => (_bloomFilter2 & mask) != 0,
_ => (_bloomFilter3 & mask) != 0,
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void BloomFilterAdd(int hash)
{
var h1 = hash & BloomFilterMask;
var h2 = (hash >> 8) & BloomFilterMask;
BloomFilterSetBit(h1);
BloomFilterSetBit(h2);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void BloomFilterSetBit(int bit)
{
var segment = bit >> 6;
var mask = 1UL << (bit & 63);
switch (segment)
{
case 0: _bloomFilter0 |= mask; break;
case 1: _bloomFilter1 |= mask; break;
case 2: _bloomFilter2 |= mask; break;
default: _bloomFilter3 |= mask; break;
}
}
#endregion
#region Property Name Table
@ -746,12 +646,12 @@ public static partial class AcBinarySerializer
public void WriteStringUtf8(string value)
{
// Fast path for ASCII-only strings using SIMD-optimized check
if (System.Text.Ascii.IsValid(value))
if (Ascii.IsValid(value))
{
WriteVarUInt((uint)value.Length);
EnsureCapacity(value.Length);
// Use System.Text.Ascii for SIMD-optimized ASCII to bytes conversion
System.Text.Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, value.Length), out _);
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, value.Length), out _);
_position += value.Length;
return;
}
@ -971,32 +871,28 @@ public static partial class AcBinarySerializer
#region Header and Metadata
private int _headerPosition;
private int _estimatedHeaderSize;
// Footer-based string interning: no estimation or shifting needed
// Header: [version][flags][footerPosition (4 bytes, only if string interning)]
// Body: data with StringInterned indices
// Footer: interned strings table
/// <summary>
/// Estimates header payload size based on registered property names and intern strings.
/// Call after metadata registration but before writing the body.
/// Estimates header payload size based on registered property names.
/// String interning now uses footer, so no estimation needed for strings.
/// </summary>
public int EstimateHeaderPayloadSize()
{
var size = 0;
// Only property names are in header now
if (UseMetadata && _propertyNameList is { Count: > 0 })
{
size += GetVarUIntSize((uint)_propertyNameList.Count);
foreach (var name in _propertyNameList)
for (var i = 0; i < _propertyNameList.Count; i++)
{
var byteCount = name.Length; // Assume ASCII (common case), fallback handles multi-byte
size += GetVarUIntSize((uint)byteCount) + byteCount;
}
}
if (UseStringInterning && _internedStringList is { Count: > 0 })
{
size += GetVarUIntSize((uint)_internedStringList.Count);
foreach (var value in _internedStringList)
{
var byteCount = value.Length; // Assume ASCII for estimation
var name = _propertyNameList[i];
var byteCount = name.Length; // Assume ASCII (common case)
size += GetVarUIntSize((uint)byteCount) + byteCount;
}
}
@ -1006,124 +902,107 @@ public static partial class AcBinarySerializer
public void WriteHeaderPlaceholder()
{
EnsureCapacity(2);
// Header layout:
// [0] version (1 byte)
// [1] flags (1 byte)
// [2-5] footer position (4 bytes, only if UseStringInterning)
EnsureCapacity(UseStringInterning ? 6 : 2);
_headerPosition = _position;
_position += 2;
_estimatedHeaderSize = 0;
_position += UseStringInterning ? 6 : 2;
}
/// <summary>
/// Reserves space for header based on estimation. Call after metadata registration.
/// Reserves space for property name table in header.
/// </summary>
public void ReserveHeaderSpace(int estimatedSize)
{
if (estimatedSize > 0)
{
if (estimatedSize <= 0) return;
EnsureCapacity(estimatedSize);
_estimatedHeaderSize = estimatedSize;
_position += estimatedSize;
}
}
public void FinalizeHeaderSections()
{
var hasPropertyNames = UseMetadata && _propertyNameList is { Count: > 0 };
var hasInternTable = UseStringInterning && _internedStringList is { Count: > 0 };
// Fast path: no header payload needed
if (!hasPropertyNames && !hasInternTable)
{
// Write header flags only
byte flags = BinaryTypeCode.HeaderFlagsBase;
if (UseReferenceHandling)
flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
_buffer[_headerPosition + 1] = flags;
return;
}
// Calculate actual header size using cached UTF8 bytes
var actualSize = 0;
// Calculate property names header size (strings go to footer now)
var headerPayloadSize = 0;
if (hasPropertyNames)
{
actualSize += GetVarUIntSize((uint)_propertyNameList!.Count);
foreach (var name in _propertyNameList)
headerPayloadSize += GetVarUIntSize((uint)_propertyNameList!.Count);
for (var i = 0; i < _propertyNameList.Count; i++)
{
var byteCount = System.Text.Ascii.IsValid(name) ? name.Length : Utf8NoBom.GetByteCount(name);
actualSize += GetVarUIntSize((uint)byteCount) + byteCount;
var name = _propertyNameList[i];
var byteCount = Ascii.IsValid(name) ? name.Length : Utf8NoBom.GetByteCount(name);
headerPayloadSize += GetVarUIntSize((uint)byteCount) + byteCount;
}
}
if (hasInternTable)
{
actualSize += GetVarUIntSize((uint)_internedStringList!.Count);
// Use cached UTF8 byte lengths
for (var i = 0; i < _internedStringUtf8!.Count; i++)
{
var utf8Bytes = _internedStringUtf8[i];
actualSize += GetVarUIntSize((uint)utf8Bytes.Length) + utf8Bytes.Length;
}
}
var bodyStart = _headerPosition + 2 + _estimatedHeaderSize;
var bodyLength = _position - bodyStart;
// Shift body if needed
if (actualSize != _estimatedHeaderSize && bodyLength > 0)
{
var delta = actualSize - _estimatedHeaderSize;
if (delta > 0)
{
EnsureCapacity(delta);
}
var newBodyStart = _headerPosition + 2 + actualSize;
if (delta != 0)
{
Array.Copy(_buffer, bodyStart, _buffer, newBodyStart, bodyLength);
_position += delta;
}
}
// Write header payload directly to buffer using cached UTF8 bytes
var headerPos = _headerPosition + 2;
// Write property names to header if needed
var headerPayloadStart = _headerPosition + (UseStringInterning ? 6 : 2);
if (hasPropertyNames)
{
var headerPos = headerPayloadStart;
headerPos = WriteVarUIntAt(headerPos, (uint)_propertyNameList!.Count);
foreach (var name in _propertyNameList)
for (var i = 0; i < _propertyNameList.Count; i++)
{
var name = _propertyNameList[i];
headerPos = WriteStringAtOptimized(headerPos, name);
}
}
// Footer-based string interning: write strings at the end
var footerPosition = 0;
if (hasInternTable)
{
headerPos = WriteVarUIntAt(headerPos, (uint)_internedStringList!.Count);
footerPosition = _position;
WriteFooterStrings();
}
// Write header
var flags = BinaryTypeCode.HeaderFlagsBase;
if (hasPropertyNames)
flags |= BinaryTypeCode.HeaderFlag_Metadata;
// Encode ReferenceHandlingMode using separate bits
if (ReferenceHandling == ReferenceHandlingMode.OnlyId)
flags |= BinaryTypeCode.HeaderFlag_RefHandling_OnlyId;
else if (ReferenceHandling == ReferenceHandlingMode.All)
flags |= (byte)(BinaryTypeCode.HeaderFlag_RefHandling_OnlyId | BinaryTypeCode.HeaderFlag_RefHandling_All);
// Set footer position flag if string interning is enabled
if (UseStringInterning)
flags |= BinaryTypeCode.HeaderFlag_HasFooterPosition;
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
_buffer[_headerPosition + 1] = flags;
// Always write footer position if string interning is enabled in options
// (even if there's no actual interned data - footer position will be 0)
if (UseStringInterning)
{
Unsafe.WriteUnaligned(ref _buffer[_headerPosition + 2], footerPosition);
}
}
/// <summary>
/// Writes interned strings to the footer (end of stream).
/// No shifting or estimation needed - just append.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteFooterStrings()
{
WriteVarUInt((uint)_internedStringList!.Count);
// Use cached UTF8 bytes - no re-encoding needed!
for (var i = 0; i < _internedStringUtf8!.Count; i++)
{
var utf8Bytes = _internedStringUtf8[i];
headerPos = WriteVarUIntAt(headerPos, (uint)utf8Bytes.Length);
utf8Bytes.CopyTo(_buffer.AsSpan(headerPos));
headerPos += utf8Bytes.Length;
WriteVarUInt((uint)utf8Bytes.Length);
WriteBytes(utf8Bytes);
}
}
// Write header flags
byte flags2 = BinaryTypeCode.HeaderFlagsBase;
if (hasPropertyNames)
flags2 |= BinaryTypeCode.HeaderFlag_Metadata;
if (UseReferenceHandling)
flags2 |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
if (hasInternTable)
flags2 |= BinaryTypeCode.HeaderFlag_StringInternTable;
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
_buffer[_headerPosition + 1] = flags2;
}
/// <summary>
/// Writes UTF8 string at specific position, optimized for ASCII strings.
/// </summary>
@ -1131,10 +1010,10 @@ public static partial class AcBinarySerializer
private int WriteStringAtOptimized(int pos, string value)
{
// Fast path for ASCII strings
if (System.Text.Ascii.IsValid(value))
if (Ascii.IsValid(value))
{
pos = WriteVarUIntAt(pos, (uint)value.Length);
System.Text.Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(pos, value.Length), out _);
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(pos, value.Length), out _);
return pos + value.Length;
}
// Standard path for multi-byte UTF8
@ -1173,7 +1052,7 @@ public static partial class AcBinarySerializer
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
//public bool TrackForScanningWithIId(object obj, BinarySerializeTypeMetadata metadata, out int existingRefId)
//{
// if (!UseReferenceHandling)
// if (!ReferenceHandling)
// {
// existingRefId = 0;
// return true; // No tracking needed
@ -1239,7 +1118,7 @@ public static partial class AcBinarySerializer
var length = value.Length;
EnsureCapacity(1 + length);
_buffer[_position++] = BinaryTypeCode.EncodeFixStr(length);
System.Text.Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, length), out _);
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, length), out _);
_position += length;
}

View File

@ -8,6 +8,7 @@ using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using AyCode.Core.Serializers.Jsons;
using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers.Binaries;
@ -175,10 +176,12 @@ public static partial class AcBinarySerializer
var context = BinarySerializationContextPool.Get(options);
context.WriteHeaderPlaceholder();
// Single-pass serialization - no scan phase needed!
// Reference tracking happens inline via TryTrack during WriteObject
// Single-pass serialization with footer-based string interning
// - No header size estimation needed (strings go to footer)
// - No body shifting (footer is appended at the end)
// - Reference tracking happens inline via TryTrack during WriteObject
// Estimate and reserve header space to avoid body shift later
// Reserve space only for property name table (if metadata is enabled)
var estimatedHeaderSize = context.EstimateHeaderPayloadSize();
context.ReserveHeaderSpace(estimatedHeaderSize);
@ -639,7 +642,7 @@ public static partial class AcBinarySerializer
var metadata = wrapper.Metadata;
// Single-pass reference tracking
if (context.UseReferenceHandling)
if (context.ReferenceHandling != ReferenceHandlingMode.None)
{
switch (metadata.IdAccessorType)
{

View File

@ -27,14 +27,13 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
public static readonly AcBinarySerializerOptions Default = new();
/// <summary>
/// Options optimized for maximum speed (no metadata, no interning).
/// Options optimized for maximum speed (no interning, no references).
/// Use when deserializer knows the exact type structure.
/// </summary>
public static readonly AcBinarySerializerOptions FastMode = new()
{
UseMetadata = false,
UseStringInterning = false,
UseReferenceHandling = false
ReferenceHandling = ReferenceHandlingMode.None
};
/// <summary>
@ -43,9 +42,8 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
public static readonly AcBinarySerializerOptions ShallowCopy = new()
{
MaxDepth = 0,
UseMetadata = false,
UseStringInterning = false,
UseReferenceHandling = false
ReferenceHandling = ReferenceHandlingMode.None
};
/// <summary>
@ -81,11 +79,11 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// <summary>
/// Whether to include metadata header with property names.
/// When enabled, property names are stored once and referenced by index.
/// Improves deserialization speed and allows schema evolution.
/// Default: true
/// NOTE: Currently unused - deserializer uses ordered property indices, not names.
/// Kept for potential future schema evolution support.
/// Default: false (no overhead)
/// </summary>
public bool UseMetadata { get; init; } = true;
public bool UseMetadata { get; init; } = false;
/// <summary>
/// Whether to intern repeated strings.
@ -140,7 +138,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// Creates options without reference handling.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static AcBinarySerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false };
public static AcBinarySerializerOptions WithoutReferenceHandling() => new() { ReferenceHandling = ReferenceHandlingMode.None };
/// <summary>
/// Creates options without metadata (faster but less flexible).
@ -213,10 +211,16 @@ internal static class BinaryTypeCode
// New flag-based header markers (48+) - moved to after FixStr range
// Note: FixStr range 34-65 overlaps with old HeaderFlagsBase, so headers use version byte prefix
// Header byte structure: (marker & 0xF0) == HeaderFlagsBase, flags in (marker & 0x0F)
public const byte HeaderFlagsBase = 48; // Base value for flag-based headers (0x30)
public const byte HeaderFlag_Metadata = 0x01;
public const byte HeaderFlag_ReferenceHandling = 0x02;
public const byte HeaderFlag_StringInternTable = 0x04; // String intern pool preloaded in header
public const byte HeaderFlag_Metadata = 0x01; // Bit 0: property metadata included
// Reference handling uses 2 separate bits:
// Bit 1 (0x02): OnlyId - reference handling for IId objects only
// Bit 2 (0x04): All - reference handling for all objects (includes OnlyId)
// None = both false, OnlyId = 0x02, All = 0x06 (both bits set)
public const byte HeaderFlag_RefHandling_OnlyId = 0x02;
public const byte HeaderFlag_RefHandling_All = 0x04;
public const byte HeaderFlag_HasFooterPosition = 0x08; // Bit 3: 4-byte footer position follows flags
// Compact integer variants (for VarInt optimization)
public const byte Int32Tiny = 192; // -16 to 63 stored in single byte (value = code - 192 - 16)

View File

@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using AyCode.Core.Serializers.Jsons;
namespace AyCode.Core.Serializers;
@ -40,9 +41,9 @@ public abstract class DeserializationContextBase<TMetadata> : AcSerializerContex
/// <summary>
/// Resets deserialization-specific state. Called by derived classes.
/// </summary>
public override void Reset()
public override void Reset(in AcSerializerOptions options)
{
base.Reset();
base.Reset(options);
// Future: Reset deserialization-specific state
}

View File

@ -22,11 +22,11 @@ public static partial class AcJsonDeserializer
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Return(DeserializationContext context)
public static void Return(DeserializationContext context, in AcSerializerOptions options)
{
if (Pool.Count < MaxPoolSize)
{
context.Clear();
context.Clear(null);
Pool.Enqueue(context);
}
}
@ -52,8 +52,6 @@ public static partial class AcJsonDeserializer
private List<PropertyToResolve>? _propertiesToResolve;
public bool IsMergeMode { get; set; }
public bool UseReferenceHandling { get; private set; }
public byte MaxDepth { get; private set; }
/// <summary>
/// Chain reference tracker for maintaining object identity across chain operations.
@ -77,27 +75,28 @@ public static partial class AcJsonDeserializer
Reset(options);
}
public new void Reset(in AcJsonSerializerOptions options)
public override void Reset(in AcSerializerOptions? options)
{
UseReferenceHandling = options.UseReferenceHandling;
MaxDepth = options.MaxDepth;
IsMergeMode = false;
ChainTracker = null;
_refTracker.Reset();
base.Reset(options);
}
public new void Clear()
public void Clear(in AcSerializerOptions? options)
{
base.Reset();
_refTracker.Reset();
_propertiesToResolve?.Clear();
ChainTracker = null;
Reset(options);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterObject(int id, object obj)
{
if (!UseReferenceHandling) return;
if (ReferenceHandling == ReferenceHandlingMode.None) return;
_refTracker.RegisterObject(id, obj);
}

View File

@ -59,7 +59,7 @@ public static partial class AcJsonDeserializer
try
{
if (!options.UseReferenceHandling)
if (options.ReferenceHandling == ReferenceHandlingMode.None)
{
var reader = new Utf8JsonReader(utf8Json, new JsonReaderOptions { MaxDepth = options.MaxDepth });
if (!reader.Read()) return default;
@ -77,7 +77,7 @@ public static partial class AcJsonDeserializer
}
finally
{
JsonDeserializationContextPool.Return(context);
JsonDeserializationContextPool.Return(context, options);
}
}
catch (AcJsonDeserializationException) { throw; }
@ -121,7 +121,7 @@ public static partial class AcJsonDeserializer
ValidateJson(json, targetType);
if (!options.UseReferenceHandling)
if (options.ReferenceHandling == ReferenceHandlingMode.None)
{
return DeserializeWithUtf8Reader<T>(json, options.MaxDepth);
}
@ -136,7 +136,7 @@ public static partial class AcJsonDeserializer
}
finally
{
JsonDeserializationContextPool.Return(context);
JsonDeserializationContextPool.Return(context, options);
}
}
catch (AcJsonDeserializationException) { throw; }
@ -186,7 +186,7 @@ public static partial class AcJsonDeserializer
ValidateJson(json, targetType);
// Fast path for no reference handling
if (!options.UseReferenceHandling)
if (options.ReferenceHandling == ReferenceHandlingMode.None)
{
return DeserializeWithUtf8ReaderNonGeneric(json, targetType, options.MaxDepth);
}
@ -203,7 +203,7 @@ public static partial class AcJsonDeserializer
}
finally
{
JsonDeserializationContextPool.Return(context);
JsonDeserializationContextPool.Return(context, options);
}
}
catch (AcJsonDeserializationException) { throw; }
@ -269,7 +269,7 @@ public static partial class AcJsonDeserializer
try
{
// Fast path for no reference handling
if (!options.UseReferenceHandling)
if (options.ReferenceHandling == ReferenceHandlingMode.None)
{
var reader = new Utf8JsonReader(utf8Json, new JsonReaderOptions { MaxDepth = options.MaxDepth });
if (!reader.Read()) return null;
@ -288,7 +288,7 @@ public static partial class AcJsonDeserializer
}
finally
{
JsonDeserializationContextPool.Return(context);
JsonDeserializationContextPool.Return(context, options);
}
}
catch (AcJsonDeserializationException) { throw; }
@ -363,7 +363,7 @@ public static partial class AcJsonDeserializer
ValidateJson(json, targetType);
// Fast path for no reference handling - use Utf8JsonReader streaming
if (!options.UseReferenceHandling)
if (options.ReferenceHandling == ReferenceHandlingMode.None)
{
var firstChar = json[0];
@ -418,7 +418,7 @@ public static partial class AcJsonDeserializer
}
finally
{
JsonDeserializationContextPool.Return(context);
JsonDeserializationContextPool.Return(context, options);
}
}
catch (AcJsonDeserializationException) { throw; }
@ -508,7 +508,7 @@ public static partial class AcJsonDeserializer
}
catch
{
JsonDeserializationContextPool.Return(context);
JsonDeserializationContextPool.Return(context, options);
doc.Dispose();
throw;
}
@ -621,7 +621,7 @@ public static partial class AcJsonDeserializer
if (_context != null)
{
JsonDeserializationContextPool.Return(_context);
JsonDeserializationContextPool.Return(_context, null);
_context = null;
}
_document?.Dispose();

View File

@ -43,9 +43,6 @@ public static partial class AcJsonSerializer
// Use shared reference tracker from AcSerializerCommon
private readonly AcSerializerCommon.SerializationReferenceTracker _refTracker = new();
public bool UseReferenceHandling { get; private set; }
public byte MaxDepth { get; private set; }
private static readonly JsonWriterOptions WriterOptions = new()
{
Indented = false,
@ -65,16 +62,16 @@ public static partial class AcJsonSerializer
protected override Func<Type, JsonSerializeTypeMetadata> MetadataFactory
=> static t => new JsonSerializeTypeMetadata(t);
public void Reset(in AcJsonSerializerOptions options)
public override void Reset(in AcSerializerOptions options)
{
UseReferenceHandling = options.UseReferenceHandling;
MaxDepth = options.MaxDepth;
_refTracker.Reset();
if (UseReferenceHandling)
if (ReferenceHandling != ReferenceHandlingMode.None)
{
_refTracker.EnsureInitialized();
}
base.Reset(options);
}
public void Clear()

View File

@ -58,7 +58,7 @@ public static partial class AcJsonSerializer
var context = SerializationContextPool.Get(options);
try
{
if (options.UseReferenceHandling)
if (options.ReferenceHandling != ReferenceHandlingMode.None)
ScanReferences(actualValue, context, 0);
WriteValue(actualValue, context, 0);
@ -182,7 +182,7 @@ public static partial class AcJsonSerializer
var metadata = wrapper.Metadata;
// Use IId-aware reference handling
if (context.UseReferenceHandling && context.TryGetExistingRefForIId(value, metadata, out var refId))
if (context.ReferenceHandling != ReferenceHandlingMode.None && context.TryGetExistingRefForIId(value, metadata, out var refId))
{
writer.WriteStartObject();
writer.WriteString(RefPropertyEncoded, refId.ToString(CultureInfo.InvariantCulture));
@ -192,7 +192,7 @@ public static partial class AcJsonSerializer
writer.WriteStartObject();
if (context.UseReferenceHandling && context.ShouldWriteIdForIId(value, metadata, out var id))
if (context.ReferenceHandling != ReferenceHandlingMode.None && context.ShouldWriteIdForIId(value, metadata, out var id))
{
writer.WriteString(IdPropertyEncoded, id.ToString(CultureInfo.InvariantCulture));
context.MarkAsWrittenForIId(value, metadata, id);

View File

@ -9,6 +9,27 @@ public enum AcSerializerType : byte
Toon = 2,
}
/// <summary>
/// Reference handling mode for serialization.
/// </summary>
public enum ReferenceHandlingMode : byte
{
/// <summary>
/// No reference handling - all objects serialized inline.
/// </summary>
None = 0,
/// <summary>
/// Reference handling only for IId objects - uses semantic Id for deduplication.
/// </summary>
OnlyId = 1,
/// <summary>
/// Full reference handling for all objects (future use).
/// </summary>
All = 2
}
/// <summary>
/// Delegate for custom property mapping during cross-type deserialization/population.
/// Enables mapping between different class hierarchies or renamed properties.
@ -23,10 +44,10 @@ public abstract class AcSerializerOptions
public abstract AcSerializerType SerializerType { get; init; }
/// <summary>
/// Whether to use $id/$ref reference handling for circular references.
/// Default: true
/// Reference handling mode for circular/shared references.
/// Default: OnlyId (handles IId objects)
/// </summary>
public bool UseReferenceHandling { get; init; } = true;
public ReferenceHandlingMode ReferenceHandling { get; init; } = ReferenceHandlingMode.OnlyId;
/// <summary>
/// Maximum depth for serialization/deserialization.
@ -69,7 +90,7 @@ public sealed class AcJsonSerializerOptions : AcSerializerOptions
/// <summary>
/// Options for shallow serialization (root level only, no references).
/// </summary>
public static readonly AcJsonSerializerOptions ShallowCopy = new() { MaxDepth = 0, UseReferenceHandling = false };
public static readonly AcJsonSerializerOptions ShallowCopy = new() { MaxDepth = 0, ReferenceHandling = ReferenceHandlingMode.None };
/// <summary>
/// Creates options with specified max depth.
@ -79,5 +100,5 @@ public sealed class AcJsonSerializerOptions : AcSerializerOptions
/// <summary>
/// Creates options without reference handling.
/// </summary>
public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false };
public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { ReferenceHandling = ReferenceHandlingMode.None };
}

View File

@ -1,3 +1,4 @@
using AyCode.Core.Serializers.Jsons;
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
@ -102,9 +103,9 @@ public abstract class SerializationContextBase<TMetadata> : AcSerializerContextB
/// <summary>
/// Resets serialization-specific state. Called by derived classes.
/// </summary>
public override void Reset()
public override void Reset(in AcSerializerOptions options)
{
base.Reset();
base.Reset(options);
// Future: Reset serialization-specific state
}

View File

@ -3,6 +3,7 @@ using System.Collections;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
using AyCode.Core.Serializers.Jsons;
using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers.Toons;
@ -53,7 +54,7 @@ public static partial class AcToonSerializer
}
// Check for reference
if (context.UseReferenceHandling && context.TryGetExistingRef(value, out var refId))
if (context.ReferenceHandling != ReferenceHandlingMode.None && context.TryGetExistingRef(value, out var refId))
{
context.Write($"@ref:{refId}");
return;
@ -227,7 +228,7 @@ public static partial class AcToonSerializer
var metadata = GetTypeMetadata(type);
// Write reference ID if this is a multi-referenced object
if (context.UseReferenceHandling && context.ShouldWriteRef(value, out var refId))
if (context.ReferenceHandling != ReferenceHandlingMode.None && context.ShouldWriteRef(value, out var refId))
{
context.Write($"@{refId} ");
context.MarkAsWritten(value, refId);

View File

@ -4,6 +4,8 @@ using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using AyCode.Core.Serializers.Jsons;
//using ReferenceEqualityComparer = AyCode.Core.Serializers.ReferenceEqualityComparer;
namespace AyCode.Core.Serializers.Toons;
@ -58,8 +60,6 @@ public static partial class AcToonSerializer
public AcToonSerializerOptions Options { get; private set; }
public int CurrentIndentLevel { get; set; }
public bool UseReferenceHandling { get; private set; }
public byte MaxDepth { get; private set; }
public ToonSerializationContext(AcToonSerializerOptions options)
{
@ -77,12 +77,10 @@ public static partial class AcToonSerializer
public void Reset(AcToonSerializerOptions options)
{
Options = options;
UseReferenceHandling = options.UseReferenceHandling;
MaxDepth = options.MaxDepth;
CurrentIndentLevel = 0;
_nextRefId = 1;
if (UseReferenceHandling)
if (ReferenceHandling != ReferenceHandlingMode.None)
{
_scanOccurrences ??= new Dictionary<object, int>(64, ReferenceEqualityComparer.Instance);
_writtenRefs ??= new Dictionary<object, int>(32, ReferenceEqualityComparer.Instance);
@ -90,6 +88,7 @@ public static partial class AcToonSerializer
}
_registeredTypes ??= new HashSet<Type>(16);
base.Reset(options);
}
public void Clear()

View File

@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
using AyCode.Core.Serializers.Jsons;
using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers.Toons;
@ -50,7 +51,7 @@ public static partial class AcToonSerializer
try
{
// Reference scanning if needed
if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(type))
if (context.ReferenceHandling != ReferenceHandlingMode.None && !IsPrimitiveOrStringFast(type))
{
ScanReferences(value, context, 0);
}

View File

@ -184,7 +184,7 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions
UseIndentation = true,
OmitDefaultValues = true,
WriteTypeNames = false,
UseReferenceHandling = false
ReferenceHandling = ReferenceHandlingMode.None
};
/// <summary>
@ -210,5 +210,5 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions
/// <summary>
/// Creates options without reference handling (faster, no circular reference support).
/// </summary>
public static AcToonSerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false };
public static AcToonSerializerOptions WithoutReferenceHandling() => new() { ReferenceHandling = ReferenceHandlingMode.None };
}

View File

@ -1071,6 +1071,6 @@ public class SignalRClientToHubTest_Binary_NoRef : SignalRClientToHubTestBase
{
protected override AcSerializerOptions SerializerOption { get; } = new AcBinarySerializerOptions
{
UseReferenceHandling = false
ReferenceHandling = ReferenceHandlingMode.None
};
}

View File

@ -8,7 +8,7 @@ namespace AyCode.Services.Server.Tests.SignalRs.SignalRDatasources;
[TestClass]
public class SignalRDataSourceTests_List_Binary_NoRef : SignalRDataSourceTestBase<TestOrderItemListDataSource, List<TestOrderItem>>
{
protected override AcSerializerOptions SerializerOption => new AcBinarySerializerOptions { UseReferenceHandling = false };
protected override AcSerializerOptions SerializerOption => new AcBinarySerializerOptions { ReferenceHandling = ReferenceHandlingMode.None };
protected override TestOrderItemListDataSource CreateDataSource(TestableSignalRClient2 client, SignalRCrudTags crudTags)
=> new(client, crudTags);