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(64, ReferenceEqualityComparer.Instance); _writtenRefs ??= new Dictionary(32, ReferenceEqualityComparer.Instance); @@ -90,6 +88,7 @@ public static partial class AcToonSerializer } _registeredTypes ??= new HashSet(16); + base.Reset(options); } public void Clear() diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.cs index 406502d..dad5523 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializer.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.cs @@ -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); } diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializerOptions.cs b/AyCode.Core/Serializers/Toons/AcToonSerializerOptions.cs index 0f75579..0e7f0c0 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializerOptions.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializerOptions.cs @@ -184,7 +184,7 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions UseIndentation = true, OmitDefaultValues = true, WriteTypeNames = false, - UseReferenceHandling = false + ReferenceHandling = ReferenceHandlingMode.None }; /// @@ -210,5 +210,5 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions /// /// Creates options without reference handling (faster, no circular reference support). /// - public static AcToonSerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false }; + public static AcToonSerializerOptions WithoutReferenceHandling() => new() { ReferenceHandling = ReferenceHandlingMode.None }; } diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs index 2c89899..e3fd54a 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs @@ -1071,6 +1071,6 @@ public class SignalRClientToHubTest_Binary_NoRef : SignalRClientToHubTestBase { protected override AcSerializerOptions SerializerOption { get; } = new AcBinarySerializerOptions { - UseReferenceHandling = false + ReferenceHandling = ReferenceHandlingMode.None }; } \ No newline at end of file diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary_NoRef.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary_NoRef.cs index 0efdd5f..2a40edb 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary_NoRef.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary_NoRef.cs @@ -8,7 +8,7 @@ namespace AyCode.Services.Server.Tests.SignalRs.SignalRDatasources; [TestClass] public class SignalRDataSourceTests_List_Binary_NoRef : SignalRDataSourceTestBase> { - 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);