Refactor serializer options, string fast paths & analysis
- Refactor all serializer options to use properties returning new instances (no shared mutable state); update all usages accordingly - Extract AcSerializerOptions, BinaryTypeCode, and BinaryPropertyFilterContext to dedicated files for clarity and reuse - Add DEBUG-only string interning analysis/reporting tools to AcBinarySerializer - Improve AcBinarySerializer string property serialization with direct typed getter and SIMD-optimized ASCII path - Increase benchmark/test warmup iterations and add JIT warmup delays for more reliable performance measurements - Remove redundant usings and update documentation/comments throughout - No breaking API changes, but static readonly options fields are now properties
This commit is contained in:
parent
145cc0a493
commit
1a77ee4bf9
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<PrimitiveTestClass>(_binaryData);
|
||||
|
||||
[Benchmark(Description = "JSON Deserialize")]
|
||||
public PrimitiveTestClass? DeserializeJson() => AcJsonDeserializer.Deserialize<PrimitiveTestClass>(_jsonData, AcJsonSerializerOptions.WithoutReferenceHandling());
|
||||
public PrimitiveTestClass? DeserializeJson() => AcJsonDeserializer.Deserialize<PrimitiveTestClass>(_jsonData, AcJsonSerializerOptions.WithoutReferenceHandling);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ISerializerBenchmark>
|
||||
{
|
||||
|
||||
// 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),
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<GeneratedSerializerTestModel>(bytes);
|
||||
|
||||
// Assert
|
||||
|
|
|
|||
|
|
@ -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<TestOrder>(acBinaryWithRef);
|
||||
_ = AcBinaryDeserializer.Deserialize<TestOrder>(acBinaryNoRef);
|
||||
_ = MessagePackSerializer.Deserialize<TestOrder>(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...");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Func<object, TProperty>>(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<Func<object, TProperty>>(resultExpr, objParam).Compile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using AyCode.Core.Serializers.Jsons;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
using System.Reflection;
|
||||
|
||||
namespace AyCode.Core.Serializers;
|
||||
|
||||
public abstract class AcSerializerOptions
|
||||
{
|
||||
public abstract AcSerializerType SerializerType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public ReferenceHandlingMode ReferenceHandling { get; set; } = ReferenceHandlingMode.OnlyId;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
public byte MaxDepth { get; init; } = byte.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public bool ThrowOnCircularReference { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 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>).
|
||||
/// </summary>
|
||||
public PropertyMapperDelegate? PropertyMapper { get; init; }
|
||||
}
|
||||
|
||||
public enum AcSerializerType : byte
|
||||
{
|
||||
Json = 0,
|
||||
Binary = 1,
|
||||
Toon = 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference handling mode for serialization.
|
||||
/// </summary>
|
||||
public enum ReferenceHandlingMode : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// No reference handling - all objects serialized inline.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Reference handling only for IId objects - uses semantic Id for deduplication.
|
||||
/// NOTE: Not fully implemented for JSON serializer - use All instead.
|
||||
/// Binary serializer supports this mode.
|
||||
/// </summary>
|
||||
OnlyId = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Full reference handling for all objects.
|
||||
/// </summary>
|
||||
All = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for custom property mapping during cross-type deserialization/population.
|
||||
/// Enables mapping between different class hierarchies or renamed properties.
|
||||
/// </summary>
|
||||
/// <param name="sourceProperty">Property from the source type being deserialized</param>
|
||||
/// <param name="destinationType">Target type being populated</param>
|
||||
/// <returns>Mapped destination property, or null to skip this property</returns>
|
||||
public delegate PropertyInfo? PropertyMapperDelegate(PropertyInfo sourceProperty, Type destinationType);
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/// <summary>
|
||||
/// DEBUG ONLY: Current property path being serialized (e.g., "Order.Status").
|
||||
/// Used for string interning analysis.
|
||||
/// </summary>
|
||||
internal string? CurrentPropertyPath;
|
||||
|
||||
/// <summary>
|
||||
/// DEBUG ONLY: Callback invoked when a string is registered for interning.
|
||||
/// Parameters: (propertyPath, stringValue)
|
||||
/// Use this to analyze which properties have repeated string values.
|
||||
/// </summary>
|
||||
internal Action<string?, string>? 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized FixStr write: tries SIMD ASCII conversion, falls back to UTF8.
|
||||
/// Single-pass: uses Ascii.FromUtf16 which does validation + copy.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal string write (after String type code already written).
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write short UTF8 bytes using FixStr encoding.
|
||||
/// Only call when byteLength <= 31.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="value">The object graph to analyze.</param>
|
||||
/// <param name="options">Serializer options (UseStringInterning should be enabled).</param>
|
||||
/// <returns>Dictionary of property paths to their string occurrence counts.</returns>
|
||||
public static Dictionary<string, Dictionary<string, int>> AnalyzeStringInternCandidates<T>(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<string, Dictionary<string, int>>();
|
||||
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<string, int>();
|
||||
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>(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<string, List<(string StringValue, int Count, int ByteLength)>>();
|
||||
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Serialize object to binary with default options.
|
||||
/// </summary>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
|||
|
||||
/// <summary>
|
||||
/// Default options instance with metadata and string interning enabled.
|
||||
/// Returns a new instance each time to prevent shared state corruption.
|
||||
/// </summary>
|
||||
public static readonly AcBinarySerializerOptions Default = new();
|
||||
public static AcBinarySerializerOptions Default => new();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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
|
|||
|
||||
/// <summary>
|
||||
/// Options for shallow serialization (root level only).
|
||||
/// Returns a new instance each time to prevent shared state corruption.
|
||||
/// </summary>
|
||||
public static readonly AcBinarySerializerOptions ShallowCopy = new()
|
||||
public static AcBinarySerializerOptions ShallowCopy => new()
|
||||
{
|
||||
MaxDepth = 0,
|
||||
UseStringInterning = false,
|
||||
|
|
@ -48,8 +50,9 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
|||
|
||||
/// <summary>
|
||||
/// Options optimized for WASM environment with string caching enabled.
|
||||
/// Returns a new instance each time to prevent shared state corruption.
|
||||
/// </summary>
|
||||
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
|
||||
/// </summary>
|
||||
public bool UseStringInterning { get; init; } = true;
|
||||
public bool UseStringInterning { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum string length to consider for interning.
|
||||
|
|
@ -137,8 +140,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
|||
/// <summary>
|
||||
/// Creates options without reference handling (and string interning disabled for speed).
|
||||
/// </summary>
|
||||
[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
|
|||
/// <summary>
|
||||
/// Creates options without metadata (faster but less flexible).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static AcBinarySerializerOptions WithoutMetadata() => new() { UseMetadata = false };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
/// </summary>
|
||||
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)
|
||||
|
||||
/// <summary>
|
||||
/// Check if type code represents a reference (string or object).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsReference(byte code) => code is StringInterned or ObjectRef;
|
||||
|
||||
/// <summary>
|
||||
/// Check if type code is a FixStr (short string with length encoded in type code).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsFixStr(byte code) => code is >= FixStrBase and <= FixStrMax;
|
||||
|
||||
/// <summary>
|
||||
/// Decode FixStr length from type code.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int DecodeFixStrLength(byte code) => code - FixStrBase;
|
||||
|
||||
/// <summary>
|
||||
/// Encode FixStr type code for given byte length (0-31).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte EncodeFixStr(int byteLength) => (byte)(FixStrBase + byteLength);
|
||||
|
||||
/// <summary>
|
||||
/// Check if byte length can be encoded as FixStr.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool CanEncodeAsFixStr(int byteLength) => byteLength is >= 0 and <= 31;
|
||||
|
||||
/// <summary>
|
||||
/// Check if type code is a tiny int (single byte int32 encoding).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsTinyInt(byte code) => code >= Int32Tiny;
|
||||
|
||||
/// <summary>
|
||||
/// Decode tiny int value from type code.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int DecodeTinyInt(byte code) => code - Int32Tiny - 16;
|
||||
|
||||
/// <summary>
|
||||
/// Encode small int value (-16 to 47) as type code.
|
||||
/// Returns true if value fits in tiny encoding.
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Delegate used to decide whether a property should be serialized.
|
||||
/// </summary>
|
||||
public delegate bool BinaryPropertyFilter(in BinaryPropertyFilterContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Provides property metadata and lazy value access for property filter evaluations.
|
||||
/// </summary>
|
||||
public readonly struct BinaryPropertyFilterContext
|
||||
{
|
||||
private readonly object? _instance;
|
||||
private readonly Func<object, object?>? _valueGetter;
|
||||
|
||||
internal BinaryPropertyFilterContext(object? instance, Type declaringType, string propertyName, Type propertyType, Func<object, object?>? valueGetter)
|
||||
{
|
||||
_instance = instance;
|
||||
DeclaringType = declaringType;
|
||||
PropertyName = propertyName;
|
||||
PropertyType = propertyType;
|
||||
_valueGetter = valueGetter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the declaring type of the property.
|
||||
/// </summary>
|
||||
public Type DeclaringType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property name.
|
||||
/// </summary>
|
||||
public string PropertyName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property type.
|
||||
/// </summary>
|
||||
public Type PropertyType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the instance being serialized when available. Null during metadata registration.
|
||||
/// </summary>
|
||||
public object? Instance => _instance;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the filter is invoked during metadata registration (when no instance is available).
|
||||
/// </summary>
|
||||
public bool IsMetadataPhase => _instance is null;
|
||||
|
||||
/// <summary>
|
||||
/// Lazily obtains the current property value. Returns null when invoked during metadata registration.
|
||||
/// </summary>
|
||||
public object? GetValue()
|
||||
{
|
||||
if (_instance == null || _valueGetter == null)
|
||||
return null;
|
||||
|
||||
return _valueGetter(_instance);
|
||||
}
|
||||
}
|
||||
public static AcBinarySerializerOptions WithoutMetadata => new() { UseMetadata = false };
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
namespace AyCode.Core.Serializers.Binaries;
|
||||
|
||||
/// <summary>
|
||||
/// Delegate used to decide whether a property should be serialized.
|
||||
/// </summary>
|
||||
public delegate bool BinaryPropertyFilter(in BinaryPropertyFilterContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Provides property metadata and lazy value access for property filter evaluations.
|
||||
/// </summary>
|
||||
public readonly struct BinaryPropertyFilterContext
|
||||
{
|
||||
private readonly object? _instance;
|
||||
private readonly Func<object, object?>? _valueGetter;
|
||||
|
||||
internal BinaryPropertyFilterContext(object? instance, Type declaringType, string propertyName, Type propertyType, Func<object, object?>? valueGetter)
|
||||
{
|
||||
_instance = instance;
|
||||
DeclaringType = declaringType;
|
||||
PropertyName = propertyName;
|
||||
PropertyType = propertyType;
|
||||
_valueGetter = valueGetter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the declaring type of the property.
|
||||
/// </summary>
|
||||
public Type DeclaringType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property name.
|
||||
/// </summary>
|
||||
public string PropertyName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property type.
|
||||
/// </summary>
|
||||
public Type PropertyType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the instance being serialized when available. Null during metadata registration.
|
||||
/// </summary>
|
||||
public object? Instance => _instance;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the filter is invoked during metadata registration (when no instance is available).
|
||||
/// </summary>
|
||||
public bool IsMetadataPhase => _instance is null;
|
||||
|
||||
/// <summary>
|
||||
/// Lazily obtains the current property value. Returns null when invoked during metadata registration.
|
||||
/// </summary>
|
||||
public object? GetValue()
|
||||
{
|
||||
if (_instance == null || _valueGetter == null)
|
||||
return null;
|
||||
|
||||
return _valueGetter(_instance);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Core.Serializers.Binaries;
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
/// </summary>
|
||||
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)
|
||||
|
||||
/// <summary>
|
||||
/// Check if type code represents a reference (string or object).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsReference(byte code) => code is StringInterned or ObjectRef;
|
||||
|
||||
/// <summary>
|
||||
/// Check if type code is a FixStr (short string with length encoded in type code).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsFixStr(byte code) => code is >= FixStrBase and <= FixStrMax;
|
||||
|
||||
/// <summary>
|
||||
/// Decode FixStr length from type code.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int DecodeFixStrLength(byte code) => code - FixStrBase;
|
||||
|
||||
/// <summary>
|
||||
/// Encode FixStr type code for given byte length (0-31).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte EncodeFixStr(int byteLength) => (byte)(FixStrBase + byteLength);
|
||||
|
||||
/// <summary>
|
||||
/// Check if byte length can be encoded as FixStr.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool CanEncodeAsFixStr(int byteLength) => byteLength is >= 0 and <= 31;
|
||||
|
||||
/// <summary>
|
||||
/// Check if type code is a tiny int (single byte int32 encoding).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsTinyInt(byte code) => code >= Int32Tiny;
|
||||
|
||||
/// <summary>
|
||||
/// Decode tiny int value from type code.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int DecodeTinyInt(byte code) => code - Int32Tiny - 16;
|
||||
|
||||
/// <summary>
|
||||
/// Encode small int value (-16 to 47) as type code.
|
||||
/// Returns true if value fits in tiny encoding.
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
|
||||
namespace AyCode.Core.Serializers;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference handling mode for serialization.
|
||||
/// </summary>
|
||||
public enum ReferenceHandlingMode : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// No reference handling - all objects serialized inline.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Reference handling only for IId objects - uses semantic Id for deduplication.
|
||||
/// NOTE: Not fully implemented for JSON serializer - use All instead.
|
||||
/// Binary serializer supports this mode.
|
||||
/// </summary>
|
||||
OnlyId = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Full reference handling for all objects.
|
||||
/// </summary>
|
||||
All = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for custom property mapping during cross-type deserialization/population.
|
||||
/// Enables mapping between different class hierarchies or renamed properties.
|
||||
/// </summary>
|
||||
/// <param name="sourceProperty">Property from the source type being deserialized</param>
|
||||
/// <param name="destinationType">Target type being populated</param>
|
||||
/// <returns>Mapped destination property, or null to skip this property</returns>
|
||||
public delegate PropertyInfo? PropertyMapperDelegate(PropertyInfo sourceProperty, Type destinationType);
|
||||
|
||||
public abstract class AcSerializerOptions
|
||||
{
|
||||
public abstract AcSerializerType SerializerType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public ReferenceHandlingMode ReferenceHandling { get; set; } = ReferenceHandlingMode.OnlyId;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
public byte MaxDepth { get; init; } = byte.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public bool ThrowOnCircularReference { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 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>).
|
||||
/// </summary>
|
||||
public PropertyMapperDelegate? PropertyMapper { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for AcJsonSerializer and AcJsonDeserializer.
|
||||
/// </summary>
|
||||
|
|
@ -96,13 +11,15 @@ public sealed class AcJsonSerializerOptions : AcSerializerOptions
|
|||
|
||||
/// <summary>
|
||||
/// Default options instance with reference handling enabled and max depth.
|
||||
/// Returns a new instance each time to prevent shared state corruption.
|
||||
/// </summary>
|
||||
public static readonly AcJsonSerializerOptions Default = new() { ReferenceHandling = ReferenceHandlingMode.All };
|
||||
public static AcJsonSerializerOptions Default => new() { ReferenceHandling = ReferenceHandlingMode.All };
|
||||
|
||||
/// <summary>
|
||||
/// Options for shallow serialization (root level only, no references).
|
||||
/// Returns a new instance each time to prevent shared state corruption.
|
||||
/// </summary>
|
||||
public static readonly AcJsonSerializerOptions ShallowCopy = new() { MaxDepth = 0, ReferenceHandling = ReferenceHandlingMode.None };
|
||||
public static AcJsonSerializerOptions ShallowCopy => new() { MaxDepth = 0, ReferenceHandling = ReferenceHandlingMode.None };
|
||||
|
||||
/// <summary>
|
||||
/// Creates options with specified max depth.
|
||||
|
|
@ -112,5 +29,5 @@ public sealed class AcJsonSerializerOptions : AcSerializerOptions
|
|||
/// <summary>
|
||||
/// Creates options without reference handling.
|
||||
/// </summary>
|
||||
public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { ReferenceHandling = ReferenceHandlingMode.None };
|
||||
public static AcJsonSerializerOptions WithoutReferenceHandling => new() { ReferenceHandling = ReferenceHandlingMode.None };
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ public abstract class PropertyAccessorBase : PropertyMetadataBase
|
|||
private readonly Func<object, uint>? _uint32Getter;
|
||||
private readonly Func<object, ulong>? _uint64Getter;
|
||||
private readonly Func<object, Guid>? _guidGetter;
|
||||
private readonly Func<object, string?>? _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<string?>(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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ public enum PropertyAccessorType : byte
|
|||
UInt32,
|
||||
UInt64,
|
||||
Guid,
|
||||
Enum
|
||||
Enum,
|
||||
String
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using AyCode.Core.Serializers.Jsons;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using AyCode.Core.Serializers.Jsons;
|
||||
|
||||
namespace AyCode.Core.Serializers.Toons;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -136,8 +134,9 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions
|
|||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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.
|
||||
/// </summary>
|
||||
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.
|
||||
/// </summary>
|
||||
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
|
|||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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
|
|||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static readonly AcToonSerializerOptions Verbose = new()
|
||||
public static AcToonSerializerOptions Verbose => new()
|
||||
{
|
||||
Mode = ToonSerializationMode.Full,
|
||||
UseMeta = true,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using AyCode.Core.Helpers;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
|
||||
namespace AyCode.Core.Serializers;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using AyCode.Core.Serializers;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue