diff --git a/AyCode.Benchmark/SerializationBenchmarks.cs b/AyCode.Benchmark/SerializationBenchmarks.cs
index bcedbe8..785124a 100644
--- a/AyCode.Benchmark/SerializationBenchmarks.cs
+++ b/AyCode.Benchmark/SerializationBenchmarks.cs
@@ -535,7 +535,7 @@ public abstract class AcBinaryOptionsBenchmarkBase
{
UseMetadata = false,
UseStringInterning = false,
- UseReferenceHandling = false
+ ReferenceHandling = ReferenceHandlingMode.None,
},
_ => new AcBinarySerializerOptions()
};
diff --git a/AyCode.Core.Serializers.Console/AyCode.Core.Serializers.Console.csproj b/AyCode.Core.Serializers.Console/AyCode.Core.Serializers.Console.csproj
index acdcc68..32fbf97 100644
--- a/AyCode.Core.Serializers.Console/AyCode.Core.Serializers.Console.csproj
+++ b/AyCode.Core.Serializers.Console/AyCode.Core.Serializers.Console.csproj
@@ -7,6 +7,7 @@
+
diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs
index ca5abb4..d946dd2 100644
--- a/AyCode.Core.Serializers.Console/Program.cs
+++ b/AyCode.Core.Serializers.Console/Program.cs
@@ -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;
///
-/// 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
///
public static class Program
{
- private const int WarmupIterations = 50;
- private const int TestIterations = 5000;
-
- // 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!;
+ private const string ResultsDirectory = @"H:\Applications\Aycode\Source\AyCode.Core\Test_Benchmark_Results\Benchmark";
+
+#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();
+ 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);
+ }
+
+ // Print grouped results
+ PrintGroupedResults(allResults, testDataSets);
+
+ // Save results to file
+ SaveResults(allResults, testDataSets);
+
+ System.Console.WriteLine("\n✓ Benchmark complete!");
}
- private static void Setup()
+ #region Test Data Creation
+
+ private static List CreateTestDataSets()
+ {
+ return new List
+ {
+ 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 RunBenchmarksForTestData(TestDataSet testData, string mode)
+ {
+ var results = new List();
+ 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);
}
+
+ // 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 void RunSerializeTests()
+ private static List CreateSerializers(TestDataSet testData)
{
- System.Console.WriteLine("--- SERIALIZE ---");
-
- var sw = Stopwatch.StartNew();
- for (int i = 0; i < TestIterations; i++)
+ return new List
{
- DoSerializeAcBinary();
- }
- sw.Stop();
- System.Console.WriteLine($" AcBinary (WithRef): {sw.ElapsedMilliseconds,6:N0} ms");
-
- sw.Restart();
- for (int i = 0; i < TestIterations; i++)
- {
- DoSerializeAcBinaryNoRef();
- }
- sw.Stop();
- System.Console.WriteLine($" AcBinary (NoRef): {sw.ElapsedMilliseconds,6:N0} ms");
-
- sw.Restart();
- for (int i = 0; i < TestIterations; i++)
- {
- DoSerializeMsgPack();
- }
- sw.Stop();
- System.Console.WriteLine($" MessagePack: {sw.ElapsedMilliseconds,6:N0} ms");
+ // 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 void RunDeserializeTests()
+ private static double RunTimed(Action action, int iterations)
{
- System.Console.WriteLine("--- DESERIALIZE ---");
-
var sw = Stopwatch.StartNew();
- for (int i = 0; i < TestIterations; i++)
+ for (var i = 0; i < iterations; i++)
{
- DoDeserializeAcBinary();
+ action();
}
sw.Stop();
- System.Console.WriteLine($" AcBinary (WithRef): {sw.ElapsedMilliseconds,6:N0} ms");
-
- sw.Restart();
- for (int i = 0; i < TestIterations; i++)
- {
- DoDeserializeAcBinaryNoRef();
- }
- 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");
+ return sw.Elapsed.TotalMilliseconds;
}
- // Separate methods for better profiler visibility - NO INLINING
- [MethodImpl(MethodImplOptions.NoInlining)]
- private static byte[] DoSerializeAcBinary()
- => AcBinarySerializer.Serialize(s_testOrder, s_acBinaryOptions);
+ #endregion
- [MethodImpl(MethodImplOptions.NoInlining)]
- private static byte[] DoSerializeAcBinaryNoRef()
- => AcBinarySerializer.Serialize(s_testOrder, s_acBinaryNoRefOptions);
+ #region Serializer Implementations
- [MethodImpl(MethodImplOptions.NoInlining)]
- private static byte[] DoSerializeMsgPack()
- => MessagePackSerializer.Serialize(s_testOrder, s_msgPackOptions);
+ private interface ISerializerBenchmark
+ {
+ string Name { get; }
+ int SerializedSize { get; }
+ void Warmup(int iterations);
+ void Serialize();
+ void Deserialize();
+ }
- [MethodImpl(MethodImplOptions.NoInlining)]
- private static TestOrder? DoDeserializeAcBinary()
- => AcBinaryDeserializer.Deserialize(s_acBinaryData);
+ private sealed class AcBinaryBenchmark : ISerializerBenchmark
+ {
+ private readonly TestOrder _order;
+ private readonly AcBinarySerializerOptions _options;
+ private readonly byte[] _serialized;
- [MethodImpl(MethodImplOptions.NoInlining)]
- private static TestOrder? DoDeserializeAcBinaryNoRef()
- => AcBinaryDeserializer.Deserialize(s_acBinaryNoRefData);
+ public string Name { get; }
+ public int SerializedSize => _serialized.Length;
- [MethodImpl(MethodImplOptions.NoInlining)]
- private static TestOrder? DoDeserializeMsgPack()
- => MessagePackSerializer.Deserialize(s_msgPackData, s_msgPackOptions);
+ public AcBinaryBenchmark(TestOrder order, AcBinarySerializerOptions options, string name)
+ {
+ _order = order;
+ _options = options;
+ Name = name;
+ _serialized = AcBinarySerializer.Serialize(order, options);
+ }
+
+ public void Warmup(int iterations)
+ {
+ for (var i = 0; i < iterations; i++)
+ {
+ Serialize();
+ Deserialize();
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public void Serialize() => AcBinarySerializer.Serialize(_order, _options);
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public void Deserialize() => AcBinaryDeserializer.Deserialize(_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)]
+ public void Serialize() => AcJsonSerializer.Serialize(_order, _options);
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public void Deserialize() => AcJsonDeserializer.Deserialize(_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)]
+ public void Serialize() => MessagePackSerializer.Serialize(_order, _options);
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public void Deserialize() => MessagePackSerializer.Deserialize(_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(_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(_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 results, List 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 results, List 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
}
diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerNavigationPropertyTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerNavigationPropertyTests.cs
index 40ea031..2aaa6ab 100644
--- a/AyCode.Core.Tests/Serialization/AcBinarySerializerNavigationPropertyTests.cs
+++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerNavigationPropertyTests.cs
@@ -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)}");
}
}
diff --git a/AyCode.Core/Serializers/AcSerializerCommon.cs b/AyCode.Core/Serializers/AcSerializerCommon.cs
index 2f1dd75..24c44f4 100644
--- a/AyCode.Core/Serializers/AcSerializerCommon.cs
+++ b/AyCode.Core/Serializers/AcSerializerCommon.cs
@@ -781,7 +781,7 @@ public static class AcSerializerCommon
///
/// 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)
///
@@ -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? _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
}
///
- /// Core tracking logic using Bloom filter + HashSet.
+ /// Core tracking logic using HashSet.Add() which returns false if already exists.
///
[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(InitialCapacity);
- _seenHashes.Add(hash);
- return true;
- }
-
- // Possible duplicate - check HashSet
_seenHashes ??= new HashSet(InitialCapacity);
+
+ // HashSet.Add returns false if element already exists
if (!_seenHashes.Add(hash))
{
// Already seen - multi-referenced!
diff --git a/AyCode.Core/Serializers/AcSerializerContextBase.cs b/AyCode.Core/Serializers/AcSerializerContextBase.cs
index a3d4994..7117dee 100644
--- a/AyCode.Core/Serializers/AcSerializerContextBase.cs
+++ b/AyCode.Core/Serializers/AcSerializerContextBase.cs
@@ -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;
/// The concrete metadata type.
public abstract class AcSerializerContextBase where TMetadata : TypeMetadataBase
{
+ public byte MaxDepth { get; private set; }
+ public ReferenceHandlingMode ReferenceHandling { get; internal set; }
///
/// 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 where TMetadata : TypeM
/// Resets all wrapper tracking states for reuse.
/// Does not remove wrappers - keeps them for next operation.
///
- 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
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs
index 3c09da7..a330afb 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs
@@ -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; }
+ ///
+ /// Convenience property - true if any reference handling is enabled.
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Reads interned strings from footer position, then returns to data position.
+ /// Uses seek to footer, read strings, seek back to data.
+ ///
+ 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(internCount);
+ for (var i = 0; i < internCount; i++)
+ {
+ _internedStrings.Add(ReadHeaderString());
+ }
+
+ // Seek back to data position
+ _position = dataPosition;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs
index f4c9c53..04e34f3 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs
@@ -1,3 +1,4 @@
+using AyCode.Core.Serializers.Jsons;
using System;
using System.Buffers;
using System.Collections.Concurrent;
@@ -55,10 +56,6 @@ public static partial class AcBinarySerializer
private const int PropertyStateBufferMaxCache = 512;
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;
@@ -69,25 +66,14 @@ public static partial class AcBinarySerializer
private Dictionary? _internedStrings;
private List? _internedStringList;
-
- ///
- /// Bloom filter for quick "definitely not interned" checks.
- /// Avoids dictionary lookup for unique strings.
- ///
- private ulong _bloomFilter0;
- private ulong _bloomFilter1;
- private ulong _bloomFilter2;
- private ulong _bloomFilter3;
private Dictionary? _propertyNames;
private List? _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.Shared.Return(_buffer);
@@ -138,12 +116,6 @@ public static partial class AcBinarySerializer
public void Clear()
{
_position = 0;
-
- // Reset bloom filter
- _bloomFilter0 = 0;
- _bloomFilter1 = 0;
- _bloomFilter2 = 0;
- _bloomFilter3 = 0;
//_refTracker.Reset();
ClearAndTrimIfNeeded(_internedStrings, InitialInternCapacity * 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
///
private List? _internedStringUtf8;
+ ///
+ /// Registers a string for interning. Returns the index of the string.
+ /// Uses CollectionsMarshal.GetValueRefOrAddDefault for single-operation lookup+add.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int RegisterInternedString(string value)
{
@@ -212,32 +188,17 @@ public static partial class AcBinarySerializer
_internedStringList ??= new List(InitialInternCapacity);
_internedStringUtf8 ??= new List(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
///
- /// 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.
///
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,122 +902,105 @@ 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;
}
///
- /// Reserves space for header based on estimation. Call after metadata registration.
+ /// Reserves space for property name table in header.
///
public void ReserveHeaderSpace(int estimatedSize)
{
- if (estimatedSize > 0)
- {
- EnsureCapacity(estimatedSize);
- _estimatedHeaderSize = estimatedSize;
- _position += estimatedSize;
- }
+ if (estimatedSize <= 0) return;
+
+ EnsureCapacity(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;
- if (hasPropertyNames)
- {
- actualSize += GetVarUIntSize((uint)_propertyNameList!.Count);
- foreach (var name in _propertyNameList)
- {
- var byteCount = System.Text.Ascii.IsValid(name) ? name.Length : Utf8NoBom.GetByteCount(name);
- actualSize += 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;
+ // Calculate property names header size (strings go to footer now)
+ var headerPayloadSize = 0;
if (hasPropertyNames)
{
- headerPos = WriteVarUIntAt(headerPos, (uint)_propertyNameList!.Count);
- foreach (var name in _propertyNameList)
+ headerPayloadSize += GetVarUIntSize((uint)_propertyNameList!.Count);
+ for (var i = 0; i < _propertyNameList.Count; i++)
{
+ var name = _propertyNameList[i];
+ var byteCount = Ascii.IsValid(name) ? name.Length : Utf8NoBom.GetByteCount(name);
+ headerPayloadSize += GetVarUIntSize((uint)byteCount) + byteCount;
+ }
+ }
+
+ // Write property names to header if needed
+ var headerPayloadStart = _headerPosition + (UseStringInterning ? 6 : 2);
+ if (hasPropertyNames)
+ {
+ var headerPos = headerPayloadStart;
+ headerPos = WriteVarUIntAt(headerPos, (uint)_propertyNameList!.Count);
+ 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);
- // 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;
- }
+ footerPosition = _position;
+ WriteFooterStrings();
}
- // Write header flags
- byte flags2 = BinaryTypeCode.HeaderFlagsBase;
+ // Write header
+ var flags = BinaryTypeCode.HeaderFlagsBase;
if (hasPropertyNames)
- flags2 |= BinaryTypeCode.HeaderFlag_Metadata;
- if (UseReferenceHandling)
- flags2 |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
- if (hasInternTable)
- flags2 |= BinaryTypeCode.HeaderFlag_StringInternTable;
+ 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] = flags2;
+ _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);
+ }
+ }
+
+ ///
+ /// Writes interned strings to the footer (end of stream).
+ /// No shifting or estimation needed - just append.
+ ///
+ [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];
+ WriteVarUInt((uint)utf8Bytes.Length);
+ WriteBytes(utf8Bytes);
+ }
}
///
@@ -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;
}
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
index 1440cd6..a31b3b1 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
@@ -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)
{
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs
index 038b33e..d8238cb 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs
@@ -27,14 +27,13 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
public static readonly AcBinarySerializerOptions Default = new();
///
- /// 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.
///
public static readonly AcBinarySerializerOptions FastMode = new()
{
- UseMetadata = false,
UseStringInterning = false,
- UseReferenceHandling = false
+ ReferenceHandling = ReferenceHandlingMode.None
};
///
@@ -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
};
///
@@ -81,11 +79,11 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
///
/// 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)
///
- public bool UseMetadata { get; init; } = true;
+ public bool UseMetadata { get; init; } = false;
///
/// Whether to intern repeated strings.
@@ -140,7 +138,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// Creates options without reference handling.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static AcBinarySerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false };
+ public static AcBinarySerializerOptions WithoutReferenceHandling() => new() { ReferenceHandling = ReferenceHandlingMode.None };
///
/// 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)
diff --git a/AyCode.Core/Serializers/DeserializationContextBase.cs b/AyCode.Core/Serializers/DeserializationContextBase.cs
index 90037ef..4f37ceb 100644
--- a/AyCode.Core/Serializers/DeserializationContextBase.cs
+++ b/AyCode.Core/Serializers/DeserializationContextBase.cs
@@ -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 : AcSerializerContex
///
/// Resets deserialization-specific state. Called by derived classes.
///
- public override void Reset()
+ public override void Reset(in AcSerializerOptions options)
{
- base.Reset();
+ base.Reset(options);
// Future: Reset deserialization-specific state
}
diff --git a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializationContext.cs b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializationContext.cs
index dab5fd5..3c38f6c 100644
--- a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializationContext.cs
+++ b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializationContext.cs
@@ -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? _propertiesToResolve;
public bool IsMergeMode { get; set; }
- public bool UseReferenceHandling { get; private set; }
- public byte MaxDepth { get; private set; }
///
/// 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);
}
diff --git a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs
index b909a7e..4050e22 100644
--- a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs
+++ b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs
@@ -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(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();
diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializationContext.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializationContext.cs
index b701c5f..3f2bbfb 100644
--- a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializationContext.cs
+++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonSerializationContext.cs
@@ -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 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()
diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs
index 02e1b4b..bdaa5ac 100644
--- a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs
+++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.cs
@@ -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);
diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs
index 9a297d7..57d2fd0 100644
--- a/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs
+++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs
@@ -9,6 +9,27 @@ public enum AcSerializerType : byte
Toon = 2,
}
+///
+/// Reference handling mode for serialization.
+///
+public enum ReferenceHandlingMode : byte
+{
+ ///
+ /// No reference handling - all objects serialized inline.
+ ///
+ None = 0,
+
+ ///
+ /// Reference handling only for IId objects - uses semantic Id for deduplication.
+ ///
+ OnlyId = 1,
+
+ ///
+ /// Full reference handling for all objects (future use).
+ ///
+ All = 2
+}
+
///
/// 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; }
///
- /// Whether to use $id/$ref reference handling for circular references.
- /// Default: true
+ /// Reference handling mode for circular/shared references.
+ /// Default: OnlyId (handles IId objects)
///
- public bool UseReferenceHandling { get; init; } = true;
+ public ReferenceHandlingMode ReferenceHandling { get; init; } = ReferenceHandlingMode.OnlyId;
///
/// Maximum depth for serialization/deserialization.
@@ -69,7 +90,7 @@ public sealed class AcJsonSerializerOptions : AcSerializerOptions
///
/// Options for shallow serialization (root level only, no references).
///
- public static readonly AcJsonSerializerOptions ShallowCopy = new() { MaxDepth = 0, UseReferenceHandling = false };
+ public static readonly AcJsonSerializerOptions ShallowCopy = new() { MaxDepth = 0, ReferenceHandling = ReferenceHandlingMode.None };
///
/// Creates options with specified max depth.
@@ -79,5 +100,5 @@ public sealed class AcJsonSerializerOptions : AcSerializerOptions
///
/// Creates options without reference handling.
///
- public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false };
+ public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { ReferenceHandling = ReferenceHandlingMode.None };
}
\ No newline at end of file
diff --git a/AyCode.Core/Serializers/SerializationContextBase.cs b/AyCode.Core/Serializers/SerializationContextBase.cs
index 7f037a6..e7d2d7b 100644
--- a/AyCode.Core/Serializers/SerializationContextBase.cs
+++ b/AyCode.Core/Serializers/SerializationContextBase.cs
@@ -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 : AcSerializerContextB
///
/// Resets serialization-specific state. Called by derived classes.
///
- public override void Reset()
+ public override void Reset(in AcSerializerOptions options)
{
- base.Reset();
+ base.Reset(options);
// Future: Reset serialization-specific state
}
diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.DataSection.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.DataSection.cs
index 913adc0..705b6ce 100644
--- a/AyCode.Core/Serializers/Toons/AcToonSerializer.DataSection.cs
+++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.DataSection.cs
@@ -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);
diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs
index f088100..00c23cc 100644
--- a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs
+++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs
@@ -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