diff --git a/AyCode.Benchmark/Program.cs b/AyCode.Benchmark/Program.cs index a2a80c2..c46acb9 100644 --- a/AyCode.Benchmark/Program.cs +++ b/AyCode.Benchmark/Program.cs @@ -177,7 +177,7 @@ namespace AyCode.Benchmark // Options var withRefOptions = new AcBinarySerializerOptions(); - var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); + var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling; var msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); // Warm up @@ -364,7 +364,7 @@ namespace AyCode.Benchmark Console.WriteLine($"Created order with {order.Items.Count} items"); Console.WriteLine("\nTesting JSON serialization..."); - var jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); + var jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling; var json = AcJsonSerializer.Serialize(order, jsonOptions); // Log a quick summary to Out folder for convenience diff --git a/AyCode.Benchmark/SerializationBenchmarks.cs b/AyCode.Benchmark/SerializationBenchmarks.cs index 785124a..471eecf 100644 --- a/AyCode.Benchmark/SerializationBenchmarks.cs +++ b/AyCode.Benchmark/SerializationBenchmarks.cs @@ -14,6 +14,7 @@ using MongoDB.Bson.Serialization; using System.IO; using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Jsons; +using AyCode.Core.Serializers; namespace AyCode.Core.Benchmarks; @@ -59,23 +60,23 @@ public class SimpleBinaryBenchmark public void Setup() { _testData = TestDataFactory.CreatePrimitiveTestData(); - _binaryData = AcBinarySerializer.Serialize(_testData, AcBinarySerializerOptions.WithoutReferenceHandling()); - _jsonData = AcJsonSerializer.Serialize(_testData, AcJsonSerializerOptions.WithoutReferenceHandling()); + _binaryData = AcBinarySerializer.Serialize(_testData, AcBinarySerializerOptions.WithoutReferenceHandling); + _jsonData = AcJsonSerializer.Serialize(_testData, AcJsonSerializerOptions.WithoutReferenceHandling); Console.WriteLine($"Binary: {_binaryData.Length} bytes, JSON: {_jsonData.Length} chars"); } [Benchmark(Description = "Binary Serialize")] - public byte[] SerializeBinary() => AcBinarySerializer.Serialize(_testData, AcBinarySerializerOptions.WithoutReferenceHandling()); + public byte[] SerializeBinary() => AcBinarySerializer.Serialize(_testData, AcBinarySerializerOptions.WithoutReferenceHandling); [Benchmark(Description = "JSON Serialize", Baseline = true)] - public string SerializeJson() => AcJsonSerializer.Serialize(_testData, AcJsonSerializerOptions.WithoutReferenceHandling()); + public string SerializeJson() => AcJsonSerializer.Serialize(_testData, AcJsonSerializerOptions.WithoutReferenceHandling); [Benchmark(Description = "Binary Deserialize")] public PrimitiveTestClass? DeserializeBinary() => AcBinaryDeserializer.Deserialize(_binaryData); [Benchmark(Description = "JSON Deserialize")] - public PrimitiveTestClass? DeserializeJson() => AcJsonDeserializer.Deserialize(_jsonData, AcJsonSerializerOptions.WithoutReferenceHandling()); + public PrimitiveTestClass? DeserializeJson() => AcJsonDeserializer.Deserialize(_jsonData, AcJsonSerializerOptions.WithoutReferenceHandling); } /// @@ -105,8 +106,8 @@ public class ComplexBinaryBenchmark pointsPerMeasurement: 3); Console.WriteLine($"Created order with {_testOrder.Items.Count} items"); - _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); - _jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); + _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling; + _jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling; Console.WriteLine("Serializing AcBinary..."); _acBinaryData = AcBinarySerializer.Serialize(_testOrder, _binaryOptions); @@ -163,9 +164,9 @@ public class MessagePackComparisonBenchmark measurementsPerPallet: 2, pointsPerMeasurement: 3); - _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); + _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling; _msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); - _jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); + _jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling; _acBinaryData = AcBinarySerializer.Serialize(_testOrder, _binaryOptions); _jsonData = AcJsonSerializer.Serialize(_testOrder, _jsonOptions); @@ -276,7 +277,7 @@ public class AcBinaryVsMessagePackFullBenchmark // Setup options - WithRef uses Default (which has reference handling), NoRef explicitly disables it _withRefOptions = AcBinarySerializerOptions.Default; - _noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); + _noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling; _msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); // Serialize with different options @@ -423,8 +424,8 @@ public class SizeComparisonBenchmark public void Setup() { _msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); - _withRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); - _noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); + _withRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling; + _noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling; // Small order TestDataFactory.ResetIdCounter(); @@ -530,7 +531,7 @@ public abstract class AcBinaryOptionsBenchmarkBase private static AcBinarySerializerOptions CreateBinaryOptions(BinaryBenchmarkMode mode) => mode switch { BinaryBenchmarkMode.Default => new AcBinarySerializerOptions(), - BinaryBenchmarkMode.NoReferenceHandling => AcBinarySerializerOptions.WithoutReferenceHandling(), + BinaryBenchmarkMode.NoReferenceHandling => AcBinarySerializerOptions.WithoutReferenceHandling, BinaryBenchmarkMode.FastMode => new AcBinarySerializerOptions { UseMetadata = false, @@ -608,7 +609,7 @@ public class LargeScaleBinaryBenchmark _testOrder = TestDataFactory.CreateLargeScaleBenchmarkOrder(rootItems, pallets, measurements, points); Console.WriteLine($"Created order with {_testOrder.Items.Count} root items"); - _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); + _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling; _msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); Console.WriteLine("Serializing AcBinary..."); @@ -677,7 +678,7 @@ public class AcJsonVsSystemTextJsonBenchmark _testData = TestDataFactory.CreatePrimitiveTestData(); // Setup options - _acJsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); + _acJsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling; _stjOptions = new JsonSerializerOptions { WriteIndented = false, diff --git a/AyCode.Benchmark/SourceGeneratorBenchmarks.cs b/AyCode.Benchmark/SourceGeneratorBenchmarks.cs index 1b30b49..8372b7d 100644 --- a/AyCode.Benchmark/SourceGeneratorBenchmarks.cs +++ b/AyCode.Benchmark/SourceGeneratorBenchmarks.cs @@ -83,7 +83,7 @@ public class PureContractlessBenchmark Status = "Available" }; - _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); + _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling; _msgPackContractlessOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); _acBinaryData = AcBinarySerializer.Serialize(_testData, _binaryOptions); @@ -148,7 +148,7 @@ public class SourceGeneratorVsRuntimeBenchmark Status = "Available" }; - _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); + _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling; // MessagePack with Source Generator (uses [MessagePackObject] + [Key] attributes) _msgPackOptions = MessagePackSerializerOptions.Standard; @@ -254,7 +254,7 @@ public class RepeatedStringBenchmark Type = i % 4 == 0 ? "TypeA" : i % 4 == 1 ? "TypeB" : i % 4 == 2 ? "TypeC" : "TypeD" }).ToList(); - _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); + _binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling; _msgPackOptions = MessagePackSerializerOptions.Standard; _msgPackContractlessOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index 5fbc82b..c9b461f 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -43,7 +43,7 @@ public static class Program private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false); - private static int WarmupIterations = 5; + private static int WarmupIterations = 2000; private static int TestIterations = 1000; public static void Main(string[] args) @@ -90,8 +90,6 @@ public static class Program // Print grouped results PrintGroupedResults(allResults, testDataSets); - - // Save results to file SaveResults(allResults, testDataSets); @@ -122,7 +120,8 @@ public static class Program sharedTag: sharedTag, sharedUser: sharedUser); - var options = AcBinarySerializerOptions.WithoutReferenceHandling(); + var options = AcBinarySerializerOptions.WithoutReferenceHandling; + options.UseStringInterning = false; // Warmup (fills caches) System.Console.WriteLine("Warming up (10 iterations)..."); @@ -132,6 +131,16 @@ public static class Program } System.Console.WriteLine("Warmup complete. Caches are now populated."); System.Console.WriteLine(); + + // HOT PATH - this is what the profiler should capture! + System.Console.WriteLine("Running hot path (1000 iterations for profiling)..."); + for (var i = 0; i < 1000; i++) + { + _ = AcBinarySerializer.Serialize(order, options); + } + System.Console.WriteLine("Hot path complete."); + System.Console.WriteLine(); + System.Console.WriteLine(">>> ATTACH MEMORY PROFILER NOW <<<"); System.Console.WriteLine("Press any key to exit..."); System.Console.ReadKey(intercept: true); @@ -369,6 +378,9 @@ public static class Program serializer.Warmup(WarmupIterations); } + // Wait for tiered JIT background compilation to complete + Thread.Sleep(2000); + // Run benchmarks System.Console.WriteLine($"Running benchmarks ({TestIterations} iterations)...\n"); @@ -402,9 +414,10 @@ public static class Program { return new List { + // AcBinary variants new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault), - new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling(), SerializerAcBinaryNoRef), + new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef), new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode), new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = false }, SerializerAcBinaryNoIntern), diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerCircularReferenceTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerCircularReferenceTests.cs index cb56cc3..be9b228 100644 --- a/AyCode.Core.Tests/Serialization/AcBinarySerializerCircularReferenceTests.cs +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerCircularReferenceTests.cs @@ -1,6 +1,6 @@ using AyCode.Core.Extensions; +using AyCode.Core.Serializers; using AyCode.Core.Serializers.Binaries; -using AyCode.Core.Serializers.Jsons; using static AyCode.Core.Tests.TestModels.AcSerializerModels; namespace AyCode.Core.Tests.Serialization; diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs index 64cbfcb..6ea3363 100644 --- a/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.Linq; using AyCode.Core.Extensions; +using AyCode.Core.Serializers; using AyCode.Core.Serializers.Binaries; -using AyCode.Core.Serializers.Jsons; using AyCode.Core.Tests.TestModels; using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/AyCode.Core.Tests/Serialization/GeneratedSerializerIntegrationTests.cs b/AyCode.Core.Tests/Serialization/GeneratedSerializerIntegrationTests.cs index ee4a387..40ac6de 100644 --- a/AyCode.Core.Tests/Serialization/GeneratedSerializerIntegrationTests.cs +++ b/AyCode.Core.Tests/Serialization/GeneratedSerializerIntegrationTests.cs @@ -109,7 +109,7 @@ public class GeneratedSerializerIntegrationTests }; // Serialize and deserialize using the regular path - var bytes = AcBinarySerializer.Serialize(original, AcBinarySerializerOptions.WithoutReferenceHandling()); + var bytes = AcBinarySerializer.Serialize(original, AcBinarySerializerOptions.WithoutReferenceHandling); var deserialized = AcBinaryDeserializer.Deserialize(bytes); // Assert diff --git a/AyCode.Core.Tests/Serialization/QuickBenchmark.cs b/AyCode.Core.Tests/Serialization/QuickBenchmark.cs index b8054bb..97c9ec1 100644 --- a/AyCode.Core.Tests/Serialization/QuickBenchmark.cs +++ b/AyCode.Core.Tests/Serialization/QuickBenchmark.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using AyCode.Core.Extensions; +using AyCode.Core.Serializers; using AyCode.Core.Serializers.Attributes; using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Jsons; @@ -12,7 +13,7 @@ namespace AyCode.Core.Tests.Serialization; [TestClass] public class QuickBenchmark { - private static readonly MessagePackSerializerOptions MsgPackOptions = + private static readonly MessagePackSerializerOptions MsgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None); private const int DefaultIterations = 1000; @@ -129,7 +130,7 @@ public class QuickBenchmark var deserializeMs = sw.Elapsed.TotalMilliseconds; // JSON comparison - var jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling(); + var jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling; sw.Restart(); string json = null!; for (int i = 0; i < iterations; i++) @@ -184,7 +185,7 @@ public class QuickBenchmark } const int iterations = DefaultIterations; - + // With interning (default) var sw = Stopwatch.StartNew(); byte[] withInterning = null!; @@ -290,7 +291,7 @@ public class QuickBenchmark Console.WriteLine($"{"Deserialize (ms)",-25} {acBinaryDeserMs,12:F2} {msgPackDeserMs,12:F2} {acBinaryDeserMs / msgPackDeserMs,9:F2}x"); Console.WriteLine($"{"Round-trip (ms)",-25} {acBinarySerMs + acBinaryDeserMs,12:F2} {msgPackSerMs + msgPackDeserMs,12:F2} {(acBinarySerMs + acBinaryDeserMs) / (msgPackSerMs + msgPackDeserMs),9:F2}x"); Console.WriteLine(); - + var sizeDiff = msgPackData.Length - acBinaryData.Length; if (sizeDiff > 0) Console.WriteLine($"[OK] AcBinary {sizeDiff:N0} bytes smaller ({100.0 * sizeDiff / msgPackData.Length:F1}% savings)"); @@ -376,6 +377,114 @@ public class QuickBenchmark #region Full Comparison (WithRef, NoRef, Populate, Merge) +#if DEBUG + [TestMethod] + public void GetAnalyzeStringInternCandidatesLog() + { + TestDataFactory.ResetIdCounter(); + var sharedTag = TestDataFactory.CreateTag("SharedTag"); + var sharedUser = TestDataFactory.CreateUser("shareduser"); + var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true); + + var testOrder = TestDataFactory.CreateOrder( + itemCount: 3, + palletsPerItem: 3, + measurementsPerPallet: 3, + pointsPerMeasurement: 4, + sharedTag: sharedTag, + sharedUser: sharedUser, + sharedMetadata: sharedMeta); + + var options = AcBinarySerializerOptions.Default; + options.ReferenceHandling = ReferenceHandlingMode.OnlyId; + var analysisLog = AcBinarySerializer.GetAnalyzeStringInternCandidatesLog(testOrder,options); + + Assert.IsNotNull(analysisLog); + Assert.IsGreaterThan(0, analysisLog.Length); + + // Print results sorted by occurrence count + Console.WriteLine(analysisLog.ToString()); + Console.WriteLine(); + } +#endif + + [TestMethod] + public void RunFullBenchmarkComparison2() + { + // Create test data with shared references + TestDataFactory.ResetIdCounter(); + var sharedTag = TestDataFactory.CreateTag("SharedTag"); + var sharedUser = TestDataFactory.CreateUser("shareduser"); + var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true); + + var testOrder = TestDataFactory.CreateOrder( + itemCount: 3, + palletsPerItem: 3, + measurementsPerPallet: 3, + pointsPerMeasurement: 4, + sharedTag: sharedTag, + sharedUser: sharedUser, + sharedMetadata: sharedMeta); + + var singleOptions = AcBinarySerializerOptions.FastMode; + singleOptions.UseStringInterning = false; + + Console.WriteLine("=== MINIMAL WARMUP TEST ==="); + Console.WriteLine(); + + // Wait for tiered JIT + //Console.WriteLine("Waiting 3s for tiered JIT..."); + + // MINIMAL WARMUP: Just 1 call to populate caches + Console.WriteLine("Single warmup call (cache only)..."); + for (int i = 0; i < DefaultIterations; i++) + { + _ = AcBinarySerializer.Serialize(testOrder, singleOptions); + _ = MessagePackSerializer.Serialize(testOrder, MsgPackOptions); + } + Thread.Sleep(2000); + + Console.WriteLine(); + Console.WriteLine("--- MEASURED TESTS (5x) ---"); + + var sw = Stopwatch.StartNew(); + var results = new double[5]; + for (int test = 0; test < 5; test++) + { + sw.Restart(); + for (int i = 0; i < DefaultIterations; i++) + _ = AcBinarySerializer.Serialize(testOrder, singleOptions); + results[test] = sw.Elapsed.TotalMilliseconds; + Console.WriteLine($"AcBinary Test{test + 1}: {results[test]:F2}ms"); + } + + Console.WriteLine(); + var msgResults = new double[5]; + for (int test = 0; test < 5; test++) + { + sw.Restart(); + for (int i = 0; i < DefaultIterations; i++) + _ = MessagePackSerializer.Serialize(testOrder, MsgPackOptions); + msgResults[test] = sw.Elapsed.TotalMilliseconds; + Console.WriteLine($"MsgPack Test{test + 1}: {msgResults[test]:F2}ms"); + } + + Console.WriteLine(); + Console.WriteLine("--- ANALYSIS ---"); + var acBinaryAvg = results.Average(); + var acBinaryVariance = results.Max() - results.Min(); + var msgPackAvg = msgResults.Average(); + var msgPackVariance = msgResults.Max() - msgResults.Min(); + + Console.WriteLine($"AcBinary: avg={acBinaryAvg:F2}ms, variance={acBinaryVariance:F2}ms ({100 * acBinaryVariance / acBinaryAvg:F1}%)"); + Console.WriteLine($"MsgPack: avg={msgPackAvg:F2}ms, variance={msgPackVariance:F2}ms ({100 * msgPackVariance / msgPackAvg:F1}%)"); + + var isVarianceOk = acBinaryVariance <= acBinaryAvg * 0.2; + Console.WriteLine(isVarianceOk + ? "[OK] Minimal warmup (1 call + 3s delay) is SUFFICIENT!" + : "[PROBLEM] Minimal warmup is NOT enough - need more iterations"); + } + [TestMethod] public void RunFullBenchmarkComparison() { @@ -397,23 +506,32 @@ public class QuickBenchmark sharedMetadata: sharedMeta); // Options - var withRefOptions = new AcBinarySerializerOptions(); - var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); - - // Warmup - Console.WriteLine("\nWarming up..."); - for (int i = 0; i < 10; i++) - { - _ = AcBinarySerializer.Serialize(testOrder, withRefOptions); - _ = AcBinarySerializer.Serialize(testOrder, noRefOptions); - _ = MessagePackSerializer.Serialize(testOrder, MsgPackOptions); - } + var withRefOptions = AcBinarySerializerOptions.Default; + //withRefOptions.UseStringInterning = false; + var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling; + noRefOptions.UseStringInterning = false; // Pre-serialize var acBinaryWithRef = AcBinarySerializer.Serialize(testOrder, withRefOptions); var acBinaryNoRef = AcBinarySerializer.Serialize(testOrder, noRefOptions); var msgPackData = MessagePackSerializer.Serialize(testOrder, MsgPackOptions); + // Warmup - MUST be 1000+ iterations for tiered JIT to complete + Console.WriteLine($"\nSerialize warming up ({DefaultIterations} iterations each)..."); + for (int i = 0; i < 500; i++) + { + _ = AcBinarySerializer.Serialize(testOrder, withRefOptions); + _ = AcBinarySerializer.Serialize(testOrder, noRefOptions); + _ = MessagePackSerializer.Serialize(testOrder, MsgPackOptions); + + _ = AcBinaryDeserializer.Deserialize(acBinaryWithRef); + _ = AcBinaryDeserializer.Deserialize(acBinaryNoRef); + _ = MessagePackSerializer.Deserialize(msgPackData, MsgPackOptions); + } + + // Wait for tiered JIT background compilation to complete + Thread.Sleep(2000); + Console.WriteLine($"Iterations: {DefaultIterations:N0}"); // Size comparison @@ -519,7 +637,7 @@ public class QuickBenchmark sharedMetadata: sharedMeta); var withRefOptions = new AcBinarySerializerOptions(); - var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling(); + var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling; // Warmup Console.WriteLine("Warming up..."); @@ -602,7 +720,7 @@ public class QuickBenchmark measurementsPerPallet: 3, pointsPerMeasurement: 4); - var options = AcBinarySerializerOptions.WithoutReferenceHandling(); + var options = AcBinarySerializerOptions.WithoutReferenceHandling; var binaryData = AcBinarySerializer.Serialize(testOrder, options); Console.WriteLine("Warming up..."); diff --git a/AyCode.Core/Helpers/JsonUtilities.cs b/AyCode.Core/Helpers/JsonUtilities.cs index 1501359..339abf3 100644 --- a/AyCode.Core/Helpers/JsonUtilities.cs +++ b/AyCode.Core/Helpers/JsonUtilities.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using AyCode.Core.Interfaces; +using AyCode.Core.Serializers; using AyCode.Core.Serializers.Jsons; using Newtonsoft.Json; diff --git a/AyCode.Core/Serializers/AcSerializerCommon.cs b/AyCode.Core/Serializers/AcSerializerCommon.cs index 24c44f4..3f4e0ef 100644 --- a/AyCode.Core/Serializers/AcSerializerCommon.cs +++ b/AyCode.Core/Serializers/AcSerializerCommon.cs @@ -4,7 +4,6 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using AyCode.Core.Serializers.Expressions; -using AyCode.Core.Serializers.Jsons; using LExpression = System.Linq.Expressions.Expression; using LExpressionType = System.Linq.Expressions.ExpressionType; @@ -538,8 +537,13 @@ public static class AcSerializerCommon var objParam = LExpression.Parameter(typeof(object), "obj"); var castExpr = LExpression.Convert(objParam, declaringType); var propAccess = LExpression.Property(castExpr, prop); - var convertExpr = LExpression.Convert(propAccess, typeof(TProperty)); - return LExpression.Lambda>(convertExpr, objParam).Compile(); + + // Only convert if property type differs from TProperty (avoids unnecessary boxing) + Expression resultExpr = prop.PropertyType == typeof(TProperty) + ? propAccess + : LExpression.Convert(propAccess, typeof(TProperty)); + + return LExpression.Lambda>(resultExpr, objParam).Compile(); } /// diff --git a/AyCode.Core/Serializers/AcSerializerContextBase.cs b/AyCode.Core/Serializers/AcSerializerContextBase.cs index e2af256..29d7506 100644 --- a/AyCode.Core/Serializers/AcSerializerContextBase.cs +++ b/AyCode.Core/Serializers/AcSerializerContextBase.cs @@ -1,4 +1,3 @@ -using AyCode.Core.Serializers.Jsons; using System; using System.Collections.Concurrent; using System.Collections.Generic; diff --git a/AyCode.Core/Serializers/AcSerializerOptions.cs b/AyCode.Core/Serializers/AcSerializerOptions.cs new file mode 100644 index 0000000..f955ff7 --- /dev/null +++ b/AyCode.Core/Serializers/AcSerializerOptions.cs @@ -0,0 +1,88 @@ +using System.Reflection; + +namespace AyCode.Core.Serializers; + +public abstract class AcSerializerOptions +{ + public abstract AcSerializerType SerializerType { get; init; } + + /// + /// Reference handling mode for circular/shared references. + /// Default: OnlyId (JSON serializer requires All mode, OnlyId not yet implemented) + /// Note: Binary serializer supports OnlyId mode for IId-only tracking. + /// + public ReferenceHandlingMode ReferenceHandling { get; set; } = ReferenceHandlingMode.OnlyId; + + /// + /// Maximum depth for serialization/deserialization. + /// 0 = root level only (primitives of root object) + /// 1 = root + first level of nested objects/collections + /// byte.MaxValue (255) = effectively unlimited + /// Default: byte.MaxValue + /// + public byte MaxDepth { get; init; } = byte.MaxValue; + + /// + /// Throw exception on circular reference detection for non-IId types. + /// When true: Tracks all objects and throws InvalidOperationException on circular references. + /// When false: No tracking for non-IId types (faster, but circular refs may cause MaxDepth truncation). + /// Default: true (production safety) + /// Note: IId types are always tracked when ReferenceHandling != None. + /// + public bool ThrowOnCircularReference { get; init; } = true; + + /// + /// Optional callback for custom property mapping during cross-type operations. + /// Used when deserializing/populating with Deserialize<TSource, TDest> or Populate<TSource, TDest>. + /// + /// Use cases: + /// - Mapping between external DTOs and internal models (different class hierarchies) + /// - Handling property renames across versions + /// - Custom property pairing logic + /// + /// If null (default), properties are matched by name. + /// Callback is invoked once during mapping build phase and result is cached. + /// + /// Performance: ZERO overhead on same-type operations (Deserialize<T>). + /// + public PropertyMapperDelegate? PropertyMapper { get; init; } +} + +public enum AcSerializerType : byte +{ + Json = 0, + Binary = 1, + 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. + /// NOTE: Not fully implemented for JSON serializer - use All instead. + /// Binary serializer supports this mode. + /// + OnlyId = 1, + + /// + /// Full reference handling for all objects. + /// + All = 2 +} + +/// +/// Delegate for custom property mapping during cross-type deserialization/population. +/// Enables mapping between different class hierarchies or renamed properties. +/// +/// Property from the source type being deserialized +/// Target type being populated +/// Mapped destination property, or null to skip this property +public delegate PropertyInfo? PropertyMapperDelegate(PropertyInfo sourceProperty, Type destinationType); \ No newline at end of file diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs index ba47672..409ff0c 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs @@ -1,5 +1,4 @@ -using AyCode.Core.Serializers.Jsons; -using System; +using System; using System.Buffers.Binary; using System.Collections.Generic; using System.Runtime.CompilerServices; diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs index 00f2766..c783594 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs @@ -1,6 +1,5 @@ using System.Collections; using System.Runtime.CompilerServices; -using AyCode.Core.Serializers.Jsons; using static AyCode.Core.Serializers.AcSerializerCommon; namespace AyCode.Core.Serializers.Binaries; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 558abf8..7ac0d3c 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -1,4 +1,3 @@ -using AyCode.Core.Serializers.Jsons; using System; using System.Buffers; using System.Collections.Concurrent; @@ -77,6 +76,21 @@ public static partial class AcBinarySerializer private int[]? _propertyIndexBuffer; private byte[]? _propertyStateBuffer; +#if DEBUG + /// + /// DEBUG ONLY: Current property path being serialized (e.g., "Order.Status"). + /// Used for string interning analysis. + /// + internal string? CurrentPropertyPath; + + /// + /// DEBUG ONLY: Callback invoked when a string is registered for interning. + /// Parameters: (propertyPath, stringValue) + /// Use this to analyze which properties have repeated string values. + /// + internal Action? OnStringInterned; +#endif + // These properties delegate to Options for convenience public bool UseStringInterning => Options.UseStringInterning; public bool UseMetadata => Options.UseMetadata; @@ -1161,6 +1175,48 @@ public static partial class AcBinarySerializer _position += length; } + /// + /// Optimized FixStr write: tries SIMD ASCII conversion, falls back to UTF8. + /// Single-pass: uses Ascii.FromUtf16 which does validation + copy. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteFixStrDirect(string value) + { + var length = value.Length; + EnsureCapacity(1 + length); + + // Ascii.FromUtf16: SIMD-optimized ASCII conversion + // Returns actual bytes written - if less than input length, there was a non-ASCII char + var destSpan = _buffer.AsSpan(_position + 1, length); + var status = Ascii.FromUtf16(value.AsSpan(), destSpan, out var bytesWritten); + + if (status == System.Buffers.OperationStatus.Done && bytesWritten == length) + { + // Success - write FixStr header + _buffer[_position] = BinaryTypeCode.EncodeFixStr(length); + _position += 1 + length; + } + else + { + // Non-ASCII or partial - use standard string encoding + _buffer[_position++] = BinaryTypeCode.String; + WriteStringUtf8Internal(value); + } + } + + /// + /// Internal string write (after String type code already written). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WriteStringUtf8Internal(string value) + { + var byteCount = Utf8NoBom.GetByteCount(value); + WriteVarUInt((uint)byteCount); + EnsureCapacity(byteCount); + Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount)); + _position += byteCount; + } + /// /// Write short UTF8 bytes using FixStr encoding. /// Only call when byteLength <= 31. diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 8b5f8f4..ec5d747 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -1,4 +1,4 @@ -using AyCode.Core.Helpers; +using AyCode.Core.Helpers; using AyCode.Core.Serializers.Expressions; using System.Buffers; using System.Collections; @@ -44,6 +44,177 @@ public static partial class AcBinarySerializer #region Public API +#if DEBUG + /// + /// DEBUG ONLY: Analyzes which string properties have repeated values that would benefit from interning. + /// Returns a dictionary where key is "TypeName.PropertyName" and value is the occurrence count. + /// Only properties with count > 1 are good candidates for [StringIntern] attribute. + /// + /// The object graph to analyze. + /// Serializer options (UseStringInterning should be enabled). + /// Dictionary of property paths to their string occurrence counts. + public static Dictionary> AnalyzeStringInternCandidates(T value, AcBinarySerializerOptions? options = null) + { + ArgumentNullException.ThrowIfNull(value); + + options ??= AcBinarySerializerOptions.Default; + + // For analysis, use the provided reference handling mode + var analysisOptions = new AcBinarySerializerOptions + { + UseStringInterning = true, + MinStringInternLength = options.MinStringInternLength, + MaxStringInternLength = options.MaxStringInternLength, + ReferenceHandling = options.ReferenceHandling + }; + + var result = new Dictionary>(); + var runtimeType = value.GetType(); + + // Create context without pooling (we need to set up callback) + using var context = new BinarySerializationContext(analysisOptions); + + // Set up tracking callbacks + context.OnStringInterned = (propertyPath, stringValue) => + { + propertyPath ??= "(unknown)"; + + if (!result.TryGetValue(stringValue, out var properties)) + { + properties = new Dictionary(); + result[stringValue] = properties; + } + + properties[propertyPath] = properties.GetValueOrDefault(propertyPath) + 1; + }; + + // Run serialization to trigger callbacks + context.WriteHeaderPlaceholder(); + var estimatedHeaderSize = context.EstimateHeaderPayloadSize(); + context.ReserveHeaderSpace(estimatedHeaderSize); + WriteValue(value, runtimeType, context, 0); + context.FinalizeHeaderSections(); + + return result; + } + + public static StringBuilder GetAnalyzeStringInternCandidatesLog(T value, AcBinarySerializerOptions? options = null) + { + var sb = new StringBuilder(); + options ??= AcBinarySerializerOptions.Default; + var analysis = AnalyzeStringInternCandidates(value, options); + + // Transform: stringValue → properties TO propertyPath → (stringValue, count) + var propertyStats = new Dictionary>(); + + foreach (var (stringValue, properties) in analysis) + { + var byteLength = Encoding.UTF8.GetByteCount(stringValue); + foreach (var (propPath, count) in properties) + { + if (!propertyStats.TryGetValue(propPath, out var list)) + { + list = []; + propertyStats[propPath] = list; + } + list.Add((stringValue, count, byteLength)); + } + } + + var refMode = options.ReferenceHandling; + + // Header + sb.AppendLine("+==============================================================================+"); + sb.AppendLine($"| STRING INTERN ANALYSIS REPORT (Mode: {refMode,-12}) |"); + sb.AppendLine("+==============================================================================+"); + sb.AppendLine(); + + // Global summary + var totalStrings = analysis.Values.Sum(p => p.Values.Sum()); + var uniqueStrings = analysis.Count; + var repeatedStrings = analysis.Count(kv => kv.Value.Values.Sum() > 1); + + sb.AppendLine("+-----------------------------------------------------------------------------+"); + sb.AppendLine("| STRING SUMMARY |"); + sb.AppendLine("+-----------------------------------------------------------------------------+"); + sb.AppendLine($"| Total string occurrences: {totalStrings,-10} Unique strings: {uniqueStrings,-10} Repeated: {repeatedStrings,-8} |"); + sb.AppendLine("+-----------------------------------------------------------------------------+"); + sb.AppendLine(); + + // Property-focused table + // Calculate stats for each property first (for sorting by RepeatSum%) + var propertyStatsCalculated = propertyStats.Select(kv => + { + var propPath = kv.Key; + var strings = kv.Value; + var total = strings.Sum(s => s.Count); + var unique = strings.Count; + var repeated = strings.Count(s => s.Count > 1); + var repeatSum = strings.Where(s => s.Count > 1).Sum(s => s.Count); // Sum of occurrences of repeated strings + var repeatSumPct = total > 0 ? repeatSum * 100.0 / total : 0; + + // Calculate savings + var totalBytes = strings.Sum(s => s.Count * s.ByteLength); + var uniqueBytes = strings.Sum(s => s.ByteLength); + var indexBytes = total * 2; + var savings = totalBytes - (uniqueBytes + indexBytes); + + return (propPath, strings, total, unique, repeated, repeatSum, repeatSumPct, savings); + }).ToList(); + + sb.AppendLine("+--------------------------------+-------+---------+--------+---------+-----------+----------+-------------+"); + sb.AppendLine("| Property | Total | RepSum | Unique | Repeated| RepSum % | Savings | Recommend |"); + sb.AppendLine("+--------------------------------+-------+---------+--------+---------+-----------+----------+-------------+"); + + foreach (var stat in propertyStatsCalculated.OrderByDescending(x => x.repeatSumPct)) + { + var savingsStr = stat.savings > 0 ? $"+{stat.savings:N0}B" : $"{stat.savings:N0}B"; + var recommend = stat.savings > 100 ? "[INTERN]" : stat.savings > 0 ? " maybe " : " skip "; + + var propDisplay = stat.propPath.Length > 30 ? stat.propPath[..27] + "..." : stat.propPath; + sb.AppendLine($"| {propDisplay,-30} | {stat.total,5} | {stat.repeatSum,7} | {stat.unique,6} | {stat.repeated,7} | {stat.repeatSumPct,8:F1}% | {savingsStr,8} | {recommend,-11} |"); + } + + sb.AppendLine("+--------------------------------+-------+---------+--------+---------+-----------+----------+-------------+"); + sb.AppendLine(); + + // Detailed property breakdown (only for properties with significant savings) + sb.AppendLine("+-----------------------------------------------------------------------------+"); + sb.AppendLine("| DETAILED BREAKDOWN (properties with savings > 100 bytes) |"); + sb.AppendLine("+-----------------------------------------------------------------------------+"); + + foreach (var stat in propertyStatsCalculated + .Where(x => x.savings > 100) + .OrderByDescending(x => x.repeatSumPct)) + { + sb.AppendLine(); + sb.AppendLine($" {stat.propPath} (RepSum: {stat.repeatSum}/{stat.total} = {stat.repeatSumPct:F1}%):"); + + foreach (var (strVal, count, _) in stat.strings.OrderByDescending(s => s.Count).Take(10)) + { + var preview = strVal.Length > 40 ? strVal[..37] + "..." : strVal; + var marker = count > 1 ? ">" : " "; + sb.AppendLine($" {marker} [{count,4}x] \"{preview}\""); + } + + if (stat.strings.Count > 10) + { + sb.AppendLine($" ... and {stat.strings.Count - 10} more unique values"); + } + } + + sb.AppendLine(); + sb.AppendLine("==============================================================================="); + sb.AppendLine("Legend: Total=all occurrences, RepSum=sum of repeated string occurrences"); + sb.AppendLine(" Unique=distinct values, Repeated=count of values appearing 2+ times"); + sb.AppendLine(" RepSum%=percentage of occurrences that are repeated (higher=better for intern)"); + sb.AppendLine(" Savings=estimated bytes saved with interning (positive=good)"); + sb.AppendLine(" > = repeated string (benefits from interning)"); + + return sb; + } +#endif + /// /// Serialize object to binary with default options. /// @@ -607,19 +778,25 @@ public static partial class AcBinarySerializer && (context.MaxStringInternLength == 0 || value.Length <= context.MaxStringInternLength)) { var index = context.RegisterInternedString(value); +#if DEBUG + context.OnStringInterned?.Invoke(context.CurrentPropertyPath, value); +#endif context.WriteByte(BinaryTypeCode.StringInterned); context.WriteVarUInt((uint)index); return; } - // Try FixStr for short ASCII strings (saves 1-2 bytes per string) - if (System.Text.Ascii.IsValid(value) && BinaryTypeCode.CanEncodeAsFixStr(value.Length)) + // Fast path for short strings: check length first (cheap), then ASCII + // FixStr encodes type+length in single byte for strings <= 31 chars + var length = value.Length; + if (length <= BinaryTypeCode.FixStrMaxLength) { - context.WriteFixStr(value); + // For short strings, use direct ASCII copy (avoids double validation) + context.WriteFixStrDirect(value); return; } - // Standard string encoding + // Long strings - standard encoding context.WriteByte(BinaryTypeCode.String); context.WriteStringUtf8(value); } @@ -642,29 +819,29 @@ public static partial class AcBinarySerializer var metadata = wrapper.Metadata; // Wire format: - // - IId types: [Object][props 0-tól...] - Id a props-ban, nincs extra - // - Non-IId + All: [Object][hashcode][props 0-tól...] - hashcode elõre - // - Ref=Off: [Object][props 0-tól...] - semmi extra + // - IId types: [Object][props 0-tól...] - Id a props-ban, nincs extra + // - Non-IId + All: [Object][hashcode][props 0-tól...] - hashcode elÅ‘re + // - Ref=Off: [Object][props 0-tól...] - semmi extra // ObjectRef format: - // - IId: [ObjectRef][Id érték] + // - IId: [ObjectRef][Id érték] // - Non-IId: [ObjectRef][hashcode] if (context.UseTypeReferenceHandling(metadata)) { if (metadata.IsIId) { - // IId típus: track by Id, ObjectRef writes Id + // IId típus: track by Id, ObjectRef writes Id switch (metadata.IdAccessorType) { case AcSerializerCommon.IdAccessorType.Int32: if (!context.TryTrack(wrapper, value, out int intId)) { - // Already seen ? ObjectRef + Id + // Already seen → ObjectRef + Id context.WriteByte(BinaryTypeCode.ObjectRef); context.WriteVarInt(intId); return; } - // First occurrence ? Object (no extra data, Id in props) + // First occurrence → Object (no extra data, Id in props) context.WriteByte(BinaryTypeCode.Object); break; @@ -694,12 +871,12 @@ public static partial class AcBinarySerializer // Non-IId + RefHandling=All: track by hashcode (IdAccessorType=Int32, RefIdGetter=GetHashCode) if (!context.TryTrack(wrapper, value, out int hashcode)) { - // Already seen ? ObjectRef + hashcode + // Already seen → ObjectRef + hashcode context.WriteByte(BinaryTypeCode.ObjectRef); context.WriteVarInt(hashcode); return; } - // First occurrence ? Object + hashcode + props + // First occurrence → Object + hashcode + props context.WriteByte(BinaryTypeCode.Object); context.WriteVarInt(hashcode); } @@ -846,6 +1023,16 @@ public static partial class AcBinarySerializer context.WriteVarInt(enumValue); } return; + case PropertyAccessorType.String: + { + // Fast path: typed getter, no boxing, no Type.GetTypeCode() call + var strValue = prop.GetString(obj); + if (strValue != null) + WriteString(strValue, context); + else + context.WriteByte(BinaryTypeCode.Null); + return; + } default: // Fallback to object getter for reference types var value = prop.GetValue(obj); @@ -1002,6 +1189,23 @@ public static partial class AcBinarySerializer } return; } + case PropertyAccessorType.String: + { + // Fast path: typed getter, no boxing, no Type.GetTypeCode() call + string? value = prop.GetString(obj); + if (string.IsNullOrEmpty(value)) + { + context.WriteByte(value == null ? BinaryTypeCode.PropertySkip : BinaryTypeCode.StringEmpty); + } + else + { +#if DEBUG + context.CurrentPropertyPath = $"{prop.DeclaringType.Name}.{prop.Name}"; +#endif + WriteString(value, context); + } + return; + } default: { // Object type - use regular getter @@ -1015,6 +1219,9 @@ public static partial class AcBinarySerializer } else { +#if DEBUG + context.CurrentPropertyPath = $"{prop.DeclaringType.Name}.{prop.Name}"; +#endif WriteValue(value, prop.PropertyType, context, depth); } return; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index e5b41d7..767b510 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -1,5 +1,4 @@ using System.Runtime.CompilerServices; -using AyCode.Core.Serializers.Jsons; namespace AyCode.Core.Serializers.Binaries; @@ -23,14 +22,16 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// /// Default options instance with metadata and string interning enabled. + /// Returns a new instance each time to prevent shared state corruption. /// - public static readonly AcBinarySerializerOptions Default = new(); + public static AcBinarySerializerOptions Default => new(); /// /// Options optimized for maximum speed (no interning, no references). /// Use when deserializer knows the exact type structure. + /// Returns a new instance each time to prevent shared state corruption. /// - public static readonly AcBinarySerializerOptions FastMode = new() + public static AcBinarySerializerOptions FastMode => new() { UseStringInterning = false, ReferenceHandling = ReferenceHandlingMode.None @@ -38,8 +39,9 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// /// Options for shallow serialization (root level only). + /// Returns a new instance each time to prevent shared state corruption. /// - public static readonly AcBinarySerializerOptions ShallowCopy = new() + public static AcBinarySerializerOptions ShallowCopy => new() { MaxDepth = 0, UseStringInterning = false, @@ -48,8 +50,9 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// /// Options optimized for WASM environment with string caching enabled. + /// Returns a new instance each time to prevent shared state corruption. /// - public static readonly AcBinarySerializerOptions WasmOptimized = new() + public static AcBinarySerializerOptions WasmOptimized => new() { IsWasm = true, UseStringCaching = true @@ -91,7 +94,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// Reduces size and memory for objects with many repeated string values. /// Default: true /// - public bool UseStringInterning { get; init; } = true; + public bool UseStringInterning { get; set; } = true; /// /// Minimum string length to consider for interning. @@ -137,8 +140,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// /// Creates options without reference handling (and string interning disabled for speed). /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static AcBinarySerializerOptions WithoutReferenceHandling() => new() + public static AcBinarySerializerOptions WithoutReferenceHandling => new() { ReferenceHandling = ReferenceHandlingMode.None, }; @@ -146,213 +148,5 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// /// Creates options without metadata (faster but less flexible). /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static AcBinarySerializerOptions WithoutMetadata() => new() { UseMetadata = false }; -} - -/// -/// Binary type codes for serialization. -/// Designed for fast switch dispatch and compact storage. -/// Lower 5 bits = type code (0-31) -/// Upper 3 bits = flags (interned, reference, has-type-info) -/// -internal static class BinaryTypeCode -{ - // Primitive types (0-15) - public const byte Null = 0; - public const byte True = 1; - public const byte False = 2; - public const byte Int8 = 3; - public const byte UInt8 = 4; - public const byte Int16 = 5; - public const byte UInt16 = 6; - public const byte Int32 = 7; - public const byte UInt32 = 8; - public const byte Int64 = 9; - public const byte UInt64 = 10; - public const byte Float32 = 11; - public const byte Float64 = 12; - public const byte Decimal = 13; - public const byte Char = 14; - - // String types (16-19) - public const byte String = 16; // Inline UTF8 string - public const byte StringInterned = 17; // Reference to interned string by index - public const byte StringEmpty = 18; // Empty string marker - public const byte StringInternNew = 19; // New interned string - full content + register in table - - // Date/Time types (20-23) - public const byte DateTime = 20; - public const byte DateTimeOffset = 21; - public const byte TimeSpan = 22; - public const byte Guid = 23; - - // Enum (24) - public const byte Enum = 24; - - // Complex types (25-31) - public const byte Object = 25; // Start of object - public const byte ObjectEnd = 26; // End of object marker - public const byte ObjectRef = 27; // Reference to previously serialized object - public const byte Array = 28; // Start of array/list - public const byte Dictionary = 29; // Start of dictionary - public const byte ByteArray = 30; // Optimized byte[] storage - - // Special markers (32+, for header/meta) - // Header flags byte structure (for values >= 64): - // Bit 0 (0x01): HasMetadata - // Bit 1 (0x02): HasReferenceHandling - // Values 32, 33 are legacy for backward compatibility - public const byte MetadataHeader = 32; // Binary has metadata section (legacy, implies HasReferenceHandling=true) - public const byte NoMetadataHeader = 33; // Binary has no metadata (legacy, implies HasReferenceHandling=true) - - // FixStr range: 34-65 (32 values for strings 0-31 bytes) - // FixStr encoding: FixStrBase + length (0-31) - // This saves 1 byte for short strings by combining type + length in single byte - public const byte FixStrBase = 34; // Base value for FixStr (0xA0 in MessagePack style, but we use 34) - public const byte FixStrMax = 65; // FixStrBase + 31 = maximum FixStr code - - // 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; // 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) - public const byte Int32TinyMax = 255; // Upper bound for tiny int (192 + 64 - 1 = 255) - - // Property skip marker (for single-pass serialization optimization) - // CRITICAL: Must be in the "reserved" range 67-191 (after FixStr, before TinyInt) - // AND must not conflict with any other type codes. - // Using 191 (0xBF) - the highest value before TinyInt range starts at 192. - // This ensures it won't be confused with: - // - Primitive types (0-31) - // - FixStr (34-65) - // - TinyInt values (192-255) - public const byte PropertySkip = 191; // Marks a property with default/null value (skipped during serialization) - - /// - /// Check if type code represents a reference (string or object). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsReference(byte code) => code is StringInterned or ObjectRef; - - /// - /// Check if type code is a FixStr (short string with length encoded in type code). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsFixStr(byte code) => code is >= FixStrBase and <= FixStrMax; - - /// - /// Decode FixStr length from type code. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int DecodeFixStrLength(byte code) => code - FixStrBase; - - /// - /// Encode FixStr type code for given byte length (0-31). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static byte EncodeFixStr(int byteLength) => (byte)(FixStrBase + byteLength); - - /// - /// Check if byte length can be encoded as FixStr. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool CanEncodeAsFixStr(int byteLength) => byteLength is >= 0 and <= 31; - - /// - /// Check if type code is a tiny int (single byte int32 encoding). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsTinyInt(byte code) => code >= Int32Tiny; - - /// - /// Decode tiny int value from type code. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int DecodeTinyInt(byte code) => code - Int32Tiny - 16; - - /// - /// Encode small int value (-16 to 47) as type code. - /// Returns true if value fits in tiny encoding. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool TryEncodeTinyInt(int value, out byte code) - { - // Range: -16 to 47 (64 values total, fitting in 192-255) - if (value is >= -16 and <= 47) - { - code = (byte)(value + 16 + Int32Tiny); - return true; - } - code = 0; - return false; - } -} -/// -/// Delegate used to decide whether a property should be serialized. -/// -public delegate bool BinaryPropertyFilter(in BinaryPropertyFilterContext context); - -/// -/// Provides property metadata and lazy value access for property filter evaluations. -/// -public readonly struct BinaryPropertyFilterContext -{ - private readonly object? _instance; - private readonly Func? _valueGetter; - - internal BinaryPropertyFilterContext(object? instance, Type declaringType, string propertyName, Type propertyType, Func? valueGetter) - { - _instance = instance; - DeclaringType = declaringType; - PropertyName = propertyName; - PropertyType = propertyType; - _valueGetter = valueGetter; - } - - /// - /// Gets the declaring type of the property. - /// - public Type DeclaringType { get; } - - /// - /// Gets the property name. - /// - public string PropertyName { get; } - - /// - /// Gets the property type. - /// - public Type PropertyType { get; } - - /// - /// Gets the instance being serialized when available. Null during metadata registration. - /// - public object? Instance => _instance; - - /// - /// Indicates whether the filter is invoked during metadata registration (when no instance is available). - /// - public bool IsMetadataPhase => _instance is null; - - /// - /// Lazily obtains the current property value. Returns null when invoked during metadata registration. - /// - public object? GetValue() - { - if (_instance == null || _valueGetter == null) - return null; - - return _valueGetter(_instance); - } -} + public static AcBinarySerializerOptions WithoutMetadata => new() { UseMetadata = false }; +} \ No newline at end of file diff --git a/AyCode.Core/Serializers/Binaries/BinaryPropertyFilterContext.cs b/AyCode.Core/Serializers/Binaries/BinaryPropertyFilterContext.cs new file mode 100644 index 0000000..338a306 --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/BinaryPropertyFilterContext.cs @@ -0,0 +1,60 @@ +namespace AyCode.Core.Serializers.Binaries; + +/// +/// Delegate used to decide whether a property should be serialized. +/// +public delegate bool BinaryPropertyFilter(in BinaryPropertyFilterContext context); + +/// +/// Provides property metadata and lazy value access for property filter evaluations. +/// +public readonly struct BinaryPropertyFilterContext +{ + private readonly object? _instance; + private readonly Func? _valueGetter; + + internal BinaryPropertyFilterContext(object? instance, Type declaringType, string propertyName, Type propertyType, Func? valueGetter) + { + _instance = instance; + DeclaringType = declaringType; + PropertyName = propertyName; + PropertyType = propertyType; + _valueGetter = valueGetter; + } + + /// + /// Gets the declaring type of the property. + /// + public Type DeclaringType { get; } + + /// + /// Gets the property name. + /// + public string PropertyName { get; } + + /// + /// Gets the property type. + /// + public Type PropertyType { get; } + + /// + /// Gets the instance being serialized when available. Null during metadata registration. + /// + public object? Instance => _instance; + + /// + /// Indicates whether the filter is invoked during metadata registration (when no instance is available). + /// + public bool IsMetadataPhase => _instance is null; + + /// + /// Lazily obtains the current property value. Returns null when invoked during metadata registration. + /// + public object? GetValue() + { + if (_instance == null || _valueGetter == null) + return null; + + return _valueGetter(_instance); + } +} \ No newline at end of file diff --git a/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs b/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs new file mode 100644 index 0000000..83f1f55 --- /dev/null +++ b/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs @@ -0,0 +1,153 @@ +using System.Runtime.CompilerServices; + +namespace AyCode.Core.Serializers.Binaries; + +/// +/// Binary type codes for serialization. +/// Designed for fast switch dispatch and compact storage. +/// Lower 5 bits = type code (0-31) +/// Upper 3 bits = flags (interned, reference, has-type-info) +/// +internal static class BinaryTypeCode +{ + // Primitive types (0-15) + public const byte Null = 0; + public const byte True = 1; + public const byte False = 2; + public const byte Int8 = 3; + public const byte UInt8 = 4; + public const byte Int16 = 5; + public const byte UInt16 = 6; + public const byte Int32 = 7; + public const byte UInt32 = 8; + public const byte Int64 = 9; + public const byte UInt64 = 10; + public const byte Float32 = 11; + public const byte Float64 = 12; + public const byte Decimal = 13; + public const byte Char = 14; + + // String types (16-19) + public const byte String = 16; // Inline UTF8 string + public const byte StringInterned = 17; // Reference to interned string by index + public const byte StringEmpty = 18; // Empty string marker + public const byte StringInternNew = 19; // New interned string - full content + register in table + + // Date/Time types (20-23) + public const byte DateTime = 20; + public const byte DateTimeOffset = 21; + public const byte TimeSpan = 22; + public const byte Guid = 23; + + // Enum (24) + public const byte Enum = 24; + + // Complex types (25-31) + public const byte Object = 25; // Start of object + public const byte ObjectEnd = 26; // End of object marker + public const byte ObjectRef = 27; // Reference to previously serialized object + public const byte Array = 28; // Start of array/list + public const byte Dictionary = 29; // Start of dictionary + public const byte ByteArray = 30; // Optimized byte[] storage + + // Special markers (32+, for header/meta) + // Header flags byte structure (for values >= 64): + // Bit 0 (0x01): HasMetadata + // Bit 1 (0x02): HasReferenceHandling + // Values 32, 33 are legacy for backward compatibility + public const byte MetadataHeader = 32; // Binary has metadata section (legacy, implies HasReferenceHandling=true) + public const byte NoMetadataHeader = 33; // Binary has no metadata (legacy, implies HasReferenceHandling=true) + + // FixStr range: 34-65 (32 values for strings 0-31 bytes) + // FixStr encoding: FixStrBase + length (0-31) + // This saves 1 byte for short strings by combining type + length in single byte + public const byte FixStrBase = 34; // Base value for FixStr (0xA0 in MessagePack style, but we use 34) + public const byte FixStrMax = 65; // FixStrBase + 31 = maximum FixStr code + public const int FixStrMaxLength = 31; // Maximum string length encodable as FixStr + + // 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; // 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) + public const byte Int32TinyMax = 255; // Upper bound for tiny int (192 + 64 - 1 = 255) + + // Property skip marker (for single-pass serialization optimization) + // CRITICAL: Must be in the "reserved" range 67-191 (after FixStr, before TinyInt) + // AND must not conflict with any other type codes. + // Using 191 (0xBF) - the highest value before TinyInt range starts at 192. + // This ensures it won't be confused with: + // - Primitive types (0-31) + // - FixStr (34-65) + // - TinyInt values (192-255) + public const byte PropertySkip = 191; // Marks a property with default/null value (skipped during serialization) + + /// + /// Check if type code represents a reference (string or object). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsReference(byte code) => code is StringInterned or ObjectRef; + + /// + /// Check if type code is a FixStr (short string with length encoded in type code). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsFixStr(byte code) => code is >= FixStrBase and <= FixStrMax; + + /// + /// Decode FixStr length from type code. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int DecodeFixStrLength(byte code) => code - FixStrBase; + + /// + /// Encode FixStr type code for given byte length (0-31). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte EncodeFixStr(int byteLength) => (byte)(FixStrBase + byteLength); + + /// + /// Check if byte length can be encoded as FixStr. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CanEncodeAsFixStr(int byteLength) => byteLength is >= 0 and <= 31; + + /// + /// Check if type code is a tiny int (single byte int32 encoding). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsTinyInt(byte code) => code >= Int32Tiny; + + /// + /// Decode tiny int value from type code. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int DecodeTinyInt(byte code) => code - Int32Tiny - 16; + + /// + /// Encode small int value (-16 to 47) as type code. + /// Returns true if value fits in tiny encoding. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryEncodeTinyInt(int value, out byte code) + { + // Range: -16 to 47 (64 values total, fitting in 192-255) + if (value is >= -16 and <= 47) + { + code = (byte)(value + 16 + Int32Tiny); + return true; + } + code = 0; + return false; + } +} \ No newline at end of file diff --git a/AyCode.Core/Serializers/DeserializationContextBase.cs b/AyCode.Core/Serializers/DeserializationContextBase.cs index 67b3440..8672d49 100644 --- a/AyCode.Core/Serializers/DeserializationContextBase.cs +++ b/AyCode.Core/Serializers/DeserializationContextBase.cs @@ -1,5 +1,4 @@ using System.Runtime.CompilerServices; -using AyCode.Core.Serializers.Jsons; namespace AyCode.Core.Serializers; diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs index df7f493..ad13141 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializerOptions.cs @@ -1,92 +1,7 @@ -using System.Reflection; +using System.Reflection; namespace AyCode.Core.Serializers.Jsons; -public enum AcSerializerType : byte -{ - Json = 0, - Binary = 1, - 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. - /// NOTE: Not fully implemented for JSON serializer - use All instead. - /// Binary serializer supports this mode. - /// - OnlyId = 1, - - /// - /// Full reference handling for all objects. - /// - All = 2 -} - -/// -/// Delegate for custom property mapping during cross-type deserialization/population. -/// Enables mapping between different class hierarchies or renamed properties. -/// -/// Property from the source type being deserialized -/// Target type being populated -/// Mapped destination property, or null to skip this property -public delegate PropertyInfo? PropertyMapperDelegate(PropertyInfo sourceProperty, Type destinationType); - -public abstract class AcSerializerOptions -{ - public abstract AcSerializerType SerializerType { get; init; } - - /// - /// Reference handling mode for circular/shared references. - /// Default: OnlyId (JSON serializer requires All mode, OnlyId not yet implemented) - /// Note: Binary serializer supports OnlyId mode for IId-only tracking. - /// - public ReferenceHandlingMode ReferenceHandling { get; set; } = ReferenceHandlingMode.OnlyId; - - /// - /// Maximum depth for serialization/deserialization. - /// 0 = root level only (primitives of root object) - /// 1 = root + first level of nested objects/collections - /// byte.MaxValue (255) = effectively unlimited - /// Default: byte.MaxValue - /// - public byte MaxDepth { get; init; } = byte.MaxValue; - - /// - /// Throw exception on circular reference detection for non-IId types. - /// When true: Tracks all objects and throws InvalidOperationException on circular references. - /// When false: No tracking for non-IId types (faster, but circular refs may cause MaxDepth truncation). - /// Default: true (production safety) - /// Note: IId types are always tracked when ReferenceHandling != None. - /// - public bool ThrowOnCircularReference { get; init; } = true; - - /// - /// Optional callback for custom property mapping during cross-type operations. - /// Used when deserializing/populating with Deserialize<TSource, TDest> or Populate<TSource, TDest>. - /// - /// Use cases: - /// - Mapping between external DTOs and internal models (different class hierarchies) - /// - Handling property renames across versions - /// - Custom property pairing logic - /// - /// If null (default), properties are matched by name. - /// Callback is invoked once during mapping build phase and result is cached. - /// - /// Performance: ZERO overhead on same-type operations (Deserialize<T>). - /// - public PropertyMapperDelegate? PropertyMapper { get; init; } -} - /// /// Options for AcJsonSerializer and AcJsonDeserializer. /// @@ -96,13 +11,15 @@ public sealed class AcJsonSerializerOptions : AcSerializerOptions /// /// Default options instance with reference handling enabled and max depth. + /// Returns a new instance each time to prevent shared state corruption. /// - public static readonly AcJsonSerializerOptions Default = new() { ReferenceHandling = ReferenceHandlingMode.All }; + public static AcJsonSerializerOptions Default => new() { ReferenceHandling = ReferenceHandlingMode.All }; /// /// Options for shallow serialization (root level only, no references). + /// Returns a new instance each time to prevent shared state corruption. /// - public static readonly AcJsonSerializerOptions ShallowCopy = new() { MaxDepth = 0, ReferenceHandling = ReferenceHandlingMode.None }; + public static AcJsonSerializerOptions ShallowCopy => new() { MaxDepth = 0, ReferenceHandling = ReferenceHandlingMode.None }; /// /// Creates options with specified max depth. @@ -112,5 +29,5 @@ public sealed class AcJsonSerializerOptions : AcSerializerOptions /// /// Creates options without reference handling. /// - public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { ReferenceHandling = ReferenceHandlingMode.None }; + public static AcJsonSerializerOptions WithoutReferenceHandling => new() { ReferenceHandling = ReferenceHandlingMode.None }; } \ No newline at end of file diff --git a/AyCode.Core/Serializers/PropertyAccessorBase.cs b/AyCode.Core/Serializers/PropertyAccessorBase.cs index 77a4f85..559d216 100644 --- a/AyCode.Core/Serializers/PropertyAccessorBase.cs +++ b/AyCode.Core/Serializers/PropertyAccessorBase.cs @@ -27,6 +27,7 @@ public abstract class PropertyAccessorBase : PropertyMetadataBase private readonly Func? _uint32Getter; private readonly Func? _uint64Getter; private readonly Func? _guidGetter; + private readonly Func? _stringGetter; #endregion @@ -82,6 +83,9 @@ public abstract class PropertyAccessorBase : PropertyMetadataBase case PropertyAccessorType.Enum: Unsafe.AsRef(in _int32Getter) = AcSerializerCommon.CreateEnumGetter(declaringType, prop); break; + case PropertyAccessorType.String: + Unsafe.AsRef(in _stringGetter) = AcSerializerCommon.CreateTypedGetter(declaringType, prop); + break; } } @@ -129,5 +133,8 @@ public abstract class PropertyAccessorBase : PropertyMetadataBase [MethodImpl(MethodImplOptions.AggressiveInlining)] public int GetEnumAsInt32(object obj) => _int32Getter!(obj); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string? GetString(object obj) => _stringGetter!(obj); + #endregion } diff --git a/AyCode.Core/Serializers/PropertyMetadataBase.cs b/AyCode.Core/Serializers/PropertyMetadataBase.cs index c6a2b06..5c04d03 100644 --- a/AyCode.Core/Serializers/PropertyMetadataBase.cs +++ b/AyCode.Core/Serializers/PropertyMetadataBase.cs @@ -24,7 +24,8 @@ public enum PropertyAccessorType : byte UInt32, UInt64, Guid, - Enum + Enum, + String } /// @@ -137,6 +138,7 @@ public abstract class PropertyMetadataBase TypeCode.UInt16 => PropertyAccessorType.UInt16, TypeCode.UInt32 => PropertyAccessorType.UInt32, TypeCode.UInt64 => PropertyAccessorType.UInt64, + TypeCode.String => PropertyAccessorType.String, _ => PropertyAccessorType.Object }; } diff --git a/AyCode.Core/Serializers/SerializationContextBase.cs b/AyCode.Core/Serializers/SerializationContextBase.cs index e57eafc..82a9c8d 100644 --- a/AyCode.Core/Serializers/SerializationContextBase.cs +++ b/AyCode.Core/Serializers/SerializationContextBase.cs @@ -1,4 +1,3 @@ -using AyCode.Core.Serializers.Jsons; using System; using System.Diagnostics; using System.Runtime.CompilerServices; diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.DataSection.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.DataSection.cs index 705b6ce..53dec06 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializer.DataSection.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.DataSection.cs @@ -3,7 +3,6 @@ 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; diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs index feacfb9..7c73f27 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs @@ -4,7 +4,6 @@ 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; diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.cs index dad5523..62ca22a 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializer.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.cs @@ -3,7 +3,6 @@ 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; diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializerOptions.cs b/AyCode.Core/Serializers/Toons/AcToonSerializerOptions.cs index 0e7f0c0..d604dd6 100644 --- a/AyCode.Core/Serializers/Toons/AcToonSerializerOptions.cs +++ b/AyCode.Core/Serializers/Toons/AcToonSerializerOptions.cs @@ -1,5 +1,3 @@ -using AyCode.Core.Serializers.Jsons; - namespace AyCode.Core.Serializers.Toons; /// @@ -136,8 +134,9 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions /// /// Full mode: Meta + Data (first-time serialization). /// Use when LLM needs complete context about data structure and values. + /// Returns a new instance each time to prevent shared state corruption. /// - public static readonly AcToonSerializerOptions Default = new() + public static AcToonSerializerOptions Default => new() { Mode = ToonSerializationMode.Full, UseMeta = true, @@ -150,8 +149,9 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions /// Meta-only mode: Only serialize type definitions and descriptions. /// Use this to send schema information once at conversation start. /// Subsequent serializations can use DataOnly mode to save tokens. + /// Returns a new instance each time to prevent shared state corruption. /// - public static readonly AcToonSerializerOptions MetaOnly = new() + public static AcToonSerializerOptions MetaOnly => new() { Mode = ToonSerializationMode.MetaOnly, UseMeta = true, @@ -163,8 +163,9 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions /// Data-only mode: Only serialize actual data values. /// Use this when schema was already sent via MetaOnly. /// Saves ~30-50% tokens in repeated serializations. + /// Returns a new instance each time to prevent shared state corruption. /// - public static readonly AcToonSerializerOptions DataOnly = new() + public static AcToonSerializerOptions DataOnly => new() { Mode = ToonSerializationMode.DataOnly, UseMeta = false, @@ -176,8 +177,9 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions /// /// Compact mode: Minimal output, no meta, no indentation. /// Maximum token efficiency but less readable. + /// Returns a new instance each time to prevent shared state corruption. /// - public static readonly AcToonSerializerOptions Compact = new() + public static AcToonSerializerOptions Compact => new() { Mode = ToonSerializationMode.DataOnly, UseMeta = false, @@ -190,8 +192,9 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions /// /// Verbose mode: Everything included (for debugging/documentation). /// Use when you need maximum information and clarity. + /// Returns a new instance each time to prevent shared state corruption. /// - public static readonly AcToonSerializerOptions Verbose = new() + public static AcToonSerializerOptions Verbose => new() { Mode = ToonSerializationMode.Full, UseMeta = true, diff --git a/AyCode.Core/Serializers/TypeMetadataBase.cs b/AyCode.Core/Serializers/TypeMetadataBase.cs index 81299eb..63b4822 100644 --- a/AyCode.Core/Serializers/TypeMetadataBase.cs +++ b/AyCode.Core/Serializers/TypeMetadataBase.cs @@ -1,6 +1,5 @@ using AyCode.Core.Helpers; using AyCode.Core.Interfaces; -using AyCode.Core.Serializers.Jsons; using System; using System.Collections.Concurrent; using System.Collections.Generic; diff --git a/AyCode.Core/Serializers/TypeMetadataWrapper.cs b/AyCode.Core/Serializers/TypeMetadataWrapper.cs index 4cb9218..73ca349 100644 --- a/AyCode.Core/Serializers/TypeMetadataWrapper.cs +++ b/AyCode.Core/Serializers/TypeMetadataWrapper.cs @@ -1,7 +1,6 @@ using System; using System.Runtime.CompilerServices; using AyCode.Core.Helpers; -using AyCode.Core.Serializers.Jsons; namespace AyCode.Core.Serializers; diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs index e3fd54a..41581f2 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRClientToHubTest.cs @@ -6,6 +6,7 @@ using AyCode.Services.Server.SignalRs; using AyCode.Services.SignalRs; using MessagePack.Resolvers; using AyCode.Core.Tests.Serialization; +using AyCode.Core.Serializers; namespace AyCode.Services.Server.Tests.SignalRs; diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTestBase.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTestBase.cs index 9228ae5..5cab3af 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTestBase.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTestBase.cs @@ -1,4 +1,4 @@ -using AyCode.Core.Serializers.Jsons; +using AyCode.Core.Serializers; using AyCode.Core.Tests.TestModels; using AyCode.Services.Server.SignalRs; using AyCode.Services.SignalRs; diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary.cs index f433de1..27e04a7 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Binary.cs @@ -1,5 +1,5 @@ +using AyCode.Core.Serializers; using AyCode.Core.Serializers.Binaries; -using AyCode.Core.Serializers.Jsons; using AyCode.Core.Tests.TestModels; using AyCode.Services.SignalRs; 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 2a40edb..480c1f7 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 @@ -1,5 +1,5 @@ +using AyCode.Core.Serializers; using AyCode.Core.Serializers.Binaries; -using AyCode.Core.Serializers.Jsons; using AyCode.Core.Tests.TestModels; using AyCode.Services.SignalRs; diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Json.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Json.cs index b61f811..b353735 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Json.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_List_Json.cs @@ -1,3 +1,4 @@ +using AyCode.Core.Serializers; using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Jsons; using AyCode.Core.Tests.TestModels; diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Binary.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Binary.cs index 9cec34c..cbd8f44 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Binary.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Binary.cs @@ -1,6 +1,6 @@ using AyCode.Core.Helpers; +using AyCode.Core.Serializers; using AyCode.Core.Serializers.Binaries; -using AyCode.Core.Serializers.Jsons; using AyCode.Core.Tests.TestModels; using AyCode.Services.SignalRs; diff --git a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Json.cs b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Json.cs index c356a2d..b804455 100644 --- a/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Json.cs +++ b/AyCode.Services.Server.Tests/SignalRs/SignalRDatasources/SignalRDataSourceTests_Observable_Json.cs @@ -1,4 +1,5 @@ using AyCode.Core.Helpers; +using AyCode.Core.Serializers; using AyCode.Core.Serializers.Jsons; using AyCode.Core.Tests.TestModels; using AyCode.Services.SignalRs; diff --git a/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs b/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs index 7f666e1..8346b29 100644 --- a/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs +++ b/AyCode.Services.Server.Tests/SignalRs/TestableSignalRHub2.cs @@ -1,7 +1,7 @@ using System.Security.Claims; using AyCode.Core.Extensions; using AyCode.Core.Helpers; -using AyCode.Core.Serializers.Jsons; +using AyCode.Core.Serializers; using AyCode.Core.Tests.TestModels; using AyCode.Models.Server.DynamicMethods; using AyCode.Services.Server.SignalRs; diff --git a/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs b/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs index 967c9fc..11345db 100644 --- a/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs +++ b/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs @@ -7,8 +7,8 @@ using System.Collections; using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using AyCode.Core.Serializers.Jsons; using AyCode.Core.Compression; +using AyCode.Core.Serializers; namespace AyCode.Services.Server.SignalRs { diff --git a/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs b/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs index eef6086..e286be8 100644 --- a/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs +++ b/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs @@ -4,6 +4,7 @@ using AyCode.Core; using AyCode.Core.Extensions; using AyCode.Core.Helpers; using AyCode.Core.Loggers; +using AyCode.Core.Serializers; using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Jsons; using AyCode.Models.Server.DynamicMethods; diff --git a/AyCode.Services/SignalRs/AcSignalRClientBase.cs b/AyCode.Services/SignalRs/AcSignalRClientBase.cs index d0b8523..6cd017e 100644 --- a/AyCode.Services/SignalRs/AcSignalRClientBase.cs +++ b/AyCode.Services/SignalRs/AcSignalRClientBase.cs @@ -3,7 +3,7 @@ using AyCode.Core; using AyCode.Core.Extensions; using AyCode.Core.Helpers; using AyCode.Core.Loggers; -using AyCode.Core.Serializers.Jsons; +using AyCode.Core.Serializers; using AyCode.Interfaces.Entities; using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.SignalR.Client; diff --git a/AyCode.Services/SignalRs/IAcSignalRHubClient.cs b/AyCode.Services/SignalRs/IAcSignalRHubClient.cs index b39035a..ca69c49 100644 --- a/AyCode.Services/SignalRs/IAcSignalRHubClient.cs +++ b/AyCode.Services/SignalRs/IAcSignalRHubClient.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using AyCode.Core.Serializers.Jsons; using JsonIgnoreAttribute = Newtonsoft.Json.JsonIgnoreAttribute; using STJIgnore = System.Text.Json.Serialization.JsonIgnoreAttribute; +using AyCode.Core.Serializers; namespace AyCode.Services.SignalRs; diff --git a/AyCode.Services/SignalRs/SignalRSerializationHelper.cs b/AyCode.Services/SignalRs/SignalRSerializationHelper.cs index f3dd3f7..5c1bd45 100644 --- a/AyCode.Services/SignalRs/SignalRSerializationHelper.cs +++ b/AyCode.Services/SignalRs/SignalRSerializationHelper.cs @@ -2,6 +2,7 @@ using System.Buffers; using System.Runtime.CompilerServices; using AyCode.Core.Compression; using AyCode.Core.Extensions; +using AyCode.Core.Serializers; using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Jsons;