Refactor: unify reference handling, footer string interning, benchmarks
- Replace UseReferenceHandling bool with ReferenceHandlingMode enum across all serializers and options - Move AcBinary string interning to footer (no header shifting); remove bloom filter - Overhaul benchmark console: multi-serializer, grouped/colorized results, CSV/log output, more test data - Set UseMetadata = false by default (header property names unused) - Update all context Reset/Pool logic for new options signature - Update AcJson/Toon serializers for new reference handling - Update tests and usages for new enum-based options - Add Newtonsoft.Json to benchmark dependencies - Misc: code cleanups, improved comments, clarify logic
This commit is contained in:
parent
905b1c404d
commit
cdf3cf34f8
|
|
@ -535,7 +535,7 @@ public abstract class AcBinaryOptionsBenchmarkBase
|
|||
{
|
||||
UseMetadata = false,
|
||||
UseStringInterning = false,
|
||||
UseReferenceHandling = false
|
||||
ReferenceHandling = ReferenceHandlingMode.None,
|
||||
},
|
||||
_ => new AcBinarySerializerOptions()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MessagePack" Version="3.1.4" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -1,80 +1,128 @@
|
|||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
using MessagePack;
|
||||
using MessagePack.Resolvers;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AyCode.Core.Serializers.Console;
|
||||
|
||||
/// <summary>
|
||||
/// Console application for Performance Diagnostics profiling.
|
||||
/// Run with: Debug > Performance Profiler in Visual Studio
|
||||
/// Comprehensive benchmark application for all serializers.
|
||||
/// Compares: AcBinary (all options), AcJson, MessagePack, Newtonsoft.Json, System.Text.Json
|
||||
///
|
||||
/// Usage:
|
||||
/// dotnet run -- serialize # Profile serialize only
|
||||
/// dotnet run -- deserialize # Profile deserialize only
|
||||
/// dotnet run -- all # Profile both (default)
|
||||
/// dotnet run # Run all benchmarks
|
||||
/// dotnet run -- quick # Quick mode (fewer iterations)
|
||||
/// dotnet run -- serialize # Serialize only
|
||||
/// dotnet run -- deserialize # Deserialize only
|
||||
/// </summary>
|
||||
public static class Program
|
||||
{
|
||||
private const int WarmupIterations = 50;
|
||||
private const int TestIterations = 5000;
|
||||
private const string ResultsDirectory = @"H:\Applications\Aycode\Source\AyCode.Core\Test_Benchmark_Results\Benchmark";
|
||||
|
||||
// Keep references to prevent GC during profiling
|
||||
private static TestOrder s_testOrder = null!;
|
||||
private static byte[] s_acBinaryData = null!;
|
||||
private static byte[] s_acBinaryNoRefData = null!;
|
||||
private static byte[] s_msgPackData = null!;
|
||||
private static AcBinarySerializerOptions s_acBinaryOptions = null!;
|
||||
private static AcBinarySerializerOptions s_acBinaryNoRefOptions = null!;
|
||||
private static MessagePackSerializerOptions s_msgPackOptions = null!;
|
||||
#if DEBUG
|
||||
private const string BuildConfiguration = "Debug";
|
||||
#else
|
||||
private const string BuildConfiguration = "Release";
|
||||
#endif
|
||||
|
||||
// Serializer name constants
|
||||
private const string SerializerMessagePack = "MessagePack";
|
||||
private const string SerializerAcBinaryDefault = "AcBinary (Default)";
|
||||
private const string SerializerAcBinaryNoRef = "AcBinary (NoRef)";
|
||||
private const string SerializerAcBinaryFastMode = "AcBinary (FastMode)";
|
||||
private const string SerializerAcBinaryNoIntern = "AcBinary (NoIntern)";
|
||||
private const string SerializerAcJsonDefault = "AcJson (Default)";
|
||||
private const string SerializerNewtonsoftJson = "Newtonsoft.Json";
|
||||
private const string SerializerSystemTextJson = "System.Text.Json";
|
||||
|
||||
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
|
||||
|
||||
private static int WarmupIterations = 10;
|
||||
private static int TestIterations = 1000;
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
// Set console encoding to UTF-8 for proper Unicode character display
|
||||
System.Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
|
||||
var mode = args.Length > 0 ? args[0].ToLower() : "all";
|
||||
|
||||
System.Console.WriteLine("=".PadRight(60, '='));
|
||||
System.Console.WriteLine($"AcBinary Performance Profiler - Mode: {mode}");
|
||||
System.Console.WriteLine("=".PadRight(60, '='));
|
||||
|
||||
Setup();
|
||||
Warmup();
|
||||
|
||||
System.Console.WriteLine($"\nRunning {TestIterations} iterations...\n");
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
switch (mode)
|
||||
if (mode == "quick")
|
||||
{
|
||||
case "serialize":
|
||||
case "ser":
|
||||
RunSerializeTests();
|
||||
break;
|
||||
case "deserialize":
|
||||
case "des":
|
||||
RunDeserializeTests();
|
||||
break;
|
||||
default:
|
||||
RunSerializeTests();
|
||||
RunDeserializeTests();
|
||||
break;
|
||||
WarmupIterations = 10;
|
||||
TestIterations = 100;
|
||||
mode = "all";
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
System.Console.WriteLine($"\nTotal time: {sw.ElapsedMilliseconds:N0} ms");
|
||||
System.Console.WriteLine("=".PadRight(60, '='));
|
||||
System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════╗");
|
||||
System.Console.WriteLine("║ COMPREHENSIVE SERIALIZER BENCHMARK SUITE ║");
|
||||
System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════╝");
|
||||
System.Console.WriteLine($"Mode: {mode} | Iterations: {TestIterations} | Warmup: {WarmupIterations}");
|
||||
System.Console.WriteLine($"Build: {BuildConfiguration} | .NET: {Environment.Version}");
|
||||
System.Console.WriteLine();
|
||||
|
||||
var allResults = new List<BenchmarkResult>();
|
||||
var testDataSets = CreateTestDataSets();
|
||||
|
||||
foreach (var testData in testDataSets)
|
||||
{
|
||||
System.Console.WriteLine($"\n{'═'.ToString().PadRight(70, '═')}");
|
||||
System.Console.WriteLine($"TEST DATA: {testData.Name}");
|
||||
System.Console.WriteLine($"{'═'.ToString().PadRight(70, '═')}");
|
||||
|
||||
var results = RunBenchmarksForTestData(testData, mode);
|
||||
allResults.AddRange(results);
|
||||
}
|
||||
|
||||
private static void Setup()
|
||||
// Print grouped results
|
||||
PrintGroupedResults(allResults, testDataSets);
|
||||
|
||||
// Save results to file
|
||||
SaveResults(allResults, testDataSets);
|
||||
|
||||
System.Console.WriteLine("\n✓ Benchmark complete!");
|
||||
}
|
||||
|
||||
#region Test Data Creation
|
||||
|
||||
private static List<TestDataSet> CreateTestDataSets()
|
||||
{
|
||||
return new List<TestDataSet>
|
||||
{
|
||||
CreateSmallTestData(),
|
||||
CreateMediumTestData(),
|
||||
CreateLargeTestData(),
|
||||
CreateRepeatedStringsTestData(),
|
||||
CreateDeepNestedTestData()
|
||||
};
|
||||
}
|
||||
|
||||
private static TestDataSet CreateSmallTestData()
|
||||
{
|
||||
TestDataFactory.ResetIdCounter();
|
||||
var order = TestDataFactory.CreateOrder(
|
||||
itemCount: 2,
|
||||
palletsPerItem: 2,
|
||||
measurementsPerPallet: 2,
|
||||
pointsPerMeasurement: 2);
|
||||
|
||||
return new TestDataSet("Small (2x2x2x2)", order);
|
||||
}
|
||||
|
||||
private static TestDataSet CreateMediumTestData()
|
||||
{
|
||||
System.Console.WriteLine("Creating test data...");
|
||||
TestDataFactory.ResetIdCounter();
|
||||
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
||||
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
||||
var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);
|
||||
|
||||
s_testOrder = TestDataFactory.CreateOrder(
|
||||
var order = TestDataFactory.CreateOrder(
|
||||
itemCount: 3,
|
||||
palletsPerItem: 3,
|
||||
measurementsPerPallet: 3,
|
||||
|
|
@ -83,113 +131,632 @@ public static class Program
|
|||
sharedUser: sharedUser,
|
||||
sharedMetadata: sharedMeta);
|
||||
|
||||
s_acBinaryOptions = AcBinarySerializerOptions.Default;
|
||||
s_acBinaryNoRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
|
||||
s_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
|
||||
|
||||
s_acBinaryData = AcBinarySerializer.Serialize(s_testOrder, s_acBinaryOptions);
|
||||
s_acBinaryNoRefData = AcBinarySerializer.Serialize(s_testOrder, s_acBinaryNoRefOptions);
|
||||
s_msgPackData = MessagePackSerializer.Serialize(s_testOrder, s_msgPackOptions);
|
||||
|
||||
System.Console.WriteLine($" AcBinary (WithRef): {s_acBinaryData.Length:N0} bytes");
|
||||
System.Console.WriteLine($" AcBinary (NoRef): {s_acBinaryNoRefData.Length:N0} bytes");
|
||||
System.Console.WriteLine($" MessagePack: {s_msgPackData.Length:N0} bytes");
|
||||
return new TestDataSet("Medium (3x3x3x4, shared refs)", order);
|
||||
}
|
||||
|
||||
private static void Warmup()
|
||||
private static TestDataSet CreateLargeTestData()
|
||||
{
|
||||
TestDataFactory.ResetIdCounter();
|
||||
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
||||
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
||||
|
||||
var order = TestDataFactory.CreateOrder(
|
||||
itemCount: 5,
|
||||
palletsPerItem: 5,
|
||||
measurementsPerPallet: 5,
|
||||
pointsPerMeasurement: 10,
|
||||
sharedTag: sharedTag,
|
||||
sharedUser: sharedUser);
|
||||
|
||||
return new TestDataSet("Large (5x5x5x10)", order);
|
||||
}
|
||||
|
||||
private static TestDataSet CreateRepeatedStringsTestData()
|
||||
{
|
||||
TestDataFactory.ResetIdCounter();
|
||||
// Create order with many items to test string interning on repeated property names
|
||||
var order = TestDataFactory.CreateOrder(
|
||||
itemCount: 10,
|
||||
palletsPerItem: 2,
|
||||
measurementsPerPallet: 2,
|
||||
pointsPerMeasurement: 2);
|
||||
|
||||
// Set same status and ProductName on all items to test enum and string handling
|
||||
foreach (var item in order.Items)
|
||||
{
|
||||
item.Status = TestStatus.Processing;
|
||||
item.ProductName = "CommonProductName_RepeatedForTesting";
|
||||
}
|
||||
|
||||
return new TestDataSet("Repeated Strings (10 items)", order);
|
||||
}
|
||||
|
||||
private static TestDataSet CreateDeepNestedTestData()
|
||||
{
|
||||
TestDataFactory.ResetIdCounter();
|
||||
var order = TestDataFactory.CreateOrder(
|
||||
itemCount: 2,
|
||||
palletsPerItem: 4,
|
||||
measurementsPerPallet: 4,
|
||||
pointsPerMeasurement: 8);
|
||||
|
||||
return new TestDataSet("Deep Nested (2x4x4x8)", order);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Benchmark Execution
|
||||
|
||||
private static List<BenchmarkResult> RunBenchmarksForTestData(TestDataSet testData, string mode)
|
||||
{
|
||||
var results = new List<BenchmarkResult>();
|
||||
var serializers = CreateSerializers(testData);
|
||||
|
||||
// Warmup all serializers
|
||||
System.Console.WriteLine($"Warming up ({WarmupIterations} iterations)...");
|
||||
for (int i = 0; i < WarmupIterations; i++)
|
||||
foreach (var serializer in serializers)
|
||||
{
|
||||
DoSerializeAcBinary();
|
||||
DoSerializeAcBinaryNoRef();
|
||||
DoSerializeMsgPack();
|
||||
DoDeserializeAcBinary();
|
||||
DoDeserializeAcBinaryNoRef();
|
||||
DoDeserializeMsgPack();
|
||||
}
|
||||
serializer.Warmup(WarmupIterations);
|
||||
}
|
||||
|
||||
private static void RunSerializeTests()
|
||||
{
|
||||
System.Console.WriteLine("--- SERIALIZE ---");
|
||||
// Run benchmarks
|
||||
System.Console.WriteLine($"Running benchmarks ({TestIterations} iterations)...\n");
|
||||
|
||||
foreach (var serializer in serializers)
|
||||
{
|
||||
var result = new BenchmarkResult
|
||||
{
|
||||
TestDataName = testData.Name,
|
||||
SerializerName = serializer.Name,
|
||||
SerializedSize = serializer.SerializedSize
|
||||
};
|
||||
|
||||
if (mode is "all" or "serialize" or "ser")
|
||||
{
|
||||
result.SerializeTimeMs = RunTimed(() => serializer.Serialize(), TestIterations);
|
||||
}
|
||||
|
||||
if (mode is "all" or "deserialize" or "des")
|
||||
{
|
||||
result.DeserializeTimeMs = RunTimed(() => serializer.Deserialize(), TestIterations);
|
||||
}
|
||||
|
||||
results.Add(result);
|
||||
PrintResult(result);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static List<ISerializerBenchmark> CreateSerializers(TestDataSet testData)
|
||||
{
|
||||
return new List<ISerializerBenchmark>
|
||||
{
|
||||
// AcBinary variants
|
||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
|
||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling(), SerializerAcBinaryNoRef),
|
||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
|
||||
new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = false }, SerializerAcBinaryNoIntern),
|
||||
|
||||
// AcJson
|
||||
new AcJsonBenchmark(testData.Order, AcJsonSerializerOptions.Default, SerializerAcJsonDefault),
|
||||
|
||||
// MessagePack
|
||||
new MessagePackBenchmark(testData.Order, SerializerMessagePack),
|
||||
|
||||
// Newtonsoft.Json
|
||||
new NewtonsoftBenchmark(testData.Order, SerializerNewtonsoftJson),
|
||||
|
||||
// System.Text.Json
|
||||
new SystemTextJsonBenchmark(testData.Order, SerializerSystemTextJson)
|
||||
};
|
||||
}
|
||||
|
||||
private static double RunTimed(Action action, int iterations)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (int i = 0; i < TestIterations; i++)
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
DoSerializeAcBinary();
|
||||
action();
|
||||
}
|
||||
sw.Stop();
|
||||
System.Console.WriteLine($" AcBinary (WithRef): {sw.ElapsedMilliseconds,6:N0} ms");
|
||||
return sw.Elapsed.TotalMilliseconds;
|
||||
}
|
||||
|
||||
sw.Restart();
|
||||
for (int i = 0; i < TestIterations; i++)
|
||||
#endregion
|
||||
|
||||
#region Serializer Implementations
|
||||
|
||||
private interface ISerializerBenchmark
|
||||
{
|
||||
DoSerializeAcBinaryNoRef();
|
||||
string Name { get; }
|
||||
int SerializedSize { get; }
|
||||
void Warmup(int iterations);
|
||||
void Serialize();
|
||||
void Deserialize();
|
||||
}
|
||||
sw.Stop();
|
||||
System.Console.WriteLine($" AcBinary (NoRef): {sw.ElapsedMilliseconds,6:N0} ms");
|
||||
|
||||
sw.Restart();
|
||||
for (int i = 0; i < TestIterations; i++)
|
||||
private sealed class AcBinaryBenchmark : ISerializerBenchmark
|
||||
{
|
||||
DoSerializeMsgPack();
|
||||
}
|
||||
sw.Stop();
|
||||
System.Console.WriteLine($" MessagePack: {sw.ElapsedMilliseconds,6:N0} ms");
|
||||
}
|
||||
private readonly TestOrder _order;
|
||||
private readonly AcBinarySerializerOptions _options;
|
||||
private readonly byte[] _serialized;
|
||||
|
||||
private static void RunDeserializeTests()
|
||||
public string Name { get; }
|
||||
public int SerializedSize => _serialized.Length;
|
||||
|
||||
public AcBinaryBenchmark(TestOrder order, AcBinarySerializerOptions options, string name)
|
||||
{
|
||||
System.Console.WriteLine("--- DESERIALIZE ---");
|
||||
_order = order;
|
||||
_options = options;
|
||||
Name = name;
|
||||
_serialized = AcBinarySerializer.Serialize(order, options);
|
||||
}
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (int i = 0; i < TestIterations; i++)
|
||||
public void Warmup(int iterations)
|
||||
{
|
||||
DoDeserializeAcBinary();
|
||||
}
|
||||
sw.Stop();
|
||||
System.Console.WriteLine($" AcBinary (WithRef): {sw.ElapsedMilliseconds,6:N0} ms");
|
||||
|
||||
sw.Restart();
|
||||
for (int i = 0; i < TestIterations; i++)
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
DoDeserializeAcBinaryNoRef();
|
||||
Serialize();
|
||||
Deserialize();
|
||||
}
|
||||
sw.Stop();
|
||||
System.Console.WriteLine($" AcBinary (NoRef): {sw.ElapsedMilliseconds,6:N0} ms");
|
||||
|
||||
sw.Restart();
|
||||
for (int i = 0; i < TestIterations; i++)
|
||||
{
|
||||
DoDeserializeMsgPack();
|
||||
}
|
||||
sw.Stop();
|
||||
System.Console.WriteLine($" MessagePack: {sw.ElapsedMilliseconds,6:N0} ms");
|
||||
}
|
||||
|
||||
// Separate methods for better profiler visibility - NO INLINING
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static byte[] DoSerializeAcBinary()
|
||||
=> AcBinarySerializer.Serialize(s_testOrder, s_acBinaryOptions);
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static byte[] DoSerializeAcBinaryNoRef()
|
||||
=> AcBinarySerializer.Serialize(s_testOrder, s_acBinaryNoRefOptions);
|
||||
public void Serialize() => AcBinarySerializer.Serialize(_order, _options);
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static byte[] DoSerializeMsgPack()
|
||||
=> MessagePackSerializer.Serialize(s_testOrder, s_msgPackOptions);
|
||||
public void Deserialize() => AcBinaryDeserializer.Deserialize<TestOrder>(_serialized, _options);
|
||||
}
|
||||
|
||||
private sealed class AcJsonBenchmark : ISerializerBenchmark
|
||||
{
|
||||
private readonly TestOrder _order;
|
||||
private readonly AcJsonSerializerOptions _options;
|
||||
private readonly string _serialized;
|
||||
private readonly byte[] _serializedUtf8;
|
||||
|
||||
public string Name { get; }
|
||||
public int SerializedSize => _serializedUtf8.Length;
|
||||
|
||||
public AcJsonBenchmark(TestOrder order, AcJsonSerializerOptions options, string name)
|
||||
{
|
||||
_order = order;
|
||||
_options = options;
|
||||
Name = name;
|
||||
_serialized = AcJsonSerializer.Serialize(order, options);
|
||||
_serializedUtf8 = Utf8NoBom.GetBytes(_serialized);
|
||||
}
|
||||
|
||||
public void Warmup(int iterations)
|
||||
{
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
Serialize();
|
||||
Deserialize();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static TestOrder? DoDeserializeAcBinary()
|
||||
=> AcBinaryDeserializer.Deserialize<TestOrder>(s_acBinaryData);
|
||||
public void Serialize() => AcJsonSerializer.Serialize(_order, _options);
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static TestOrder? DoDeserializeAcBinaryNoRef()
|
||||
=> AcBinaryDeserializer.Deserialize<TestOrder>(s_acBinaryNoRefData);
|
||||
public void Deserialize() => AcJsonDeserializer.Deserialize<TestOrder>(_serialized);
|
||||
}
|
||||
|
||||
private sealed class MessagePackBenchmark : ISerializerBenchmark
|
||||
{
|
||||
private readonly TestOrder _order;
|
||||
private readonly MessagePackSerializerOptions _options;
|
||||
private readonly byte[] _serialized;
|
||||
|
||||
public string Name { get; }
|
||||
public int SerializedSize => _serialized.Length;
|
||||
|
||||
public MessagePackBenchmark(TestOrder order, string name)
|
||||
{
|
||||
_order = order;
|
||||
Name = name;
|
||||
_options = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
|
||||
_serialized = MessagePackSerializer.Serialize(order, _options);
|
||||
}
|
||||
|
||||
public void Warmup(int iterations)
|
||||
{
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
Serialize();
|
||||
Deserialize();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static TestOrder? DoDeserializeMsgPack()
|
||||
=> MessagePackSerializer.Deserialize<TestOrder>(s_msgPackData, s_msgPackOptions);
|
||||
public void Serialize() => MessagePackSerializer.Serialize(_order, _options);
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public void Deserialize() => MessagePackSerializer.Deserialize<TestOrder>(_serialized, _options);
|
||||
}
|
||||
|
||||
private sealed class NewtonsoftBenchmark : ISerializerBenchmark
|
||||
{
|
||||
private readonly TestOrder _order;
|
||||
private readonly JsonSerializerSettings _settings;
|
||||
private readonly string _serialized;
|
||||
private readonly byte[] _serializedUtf8;
|
||||
|
||||
public string Name { get; }
|
||||
public int SerializedSize => _serializedUtf8.Length;
|
||||
|
||||
public NewtonsoftBenchmark(TestOrder order, string name)
|
||||
{
|
||||
_order = order;
|
||||
Name = name;
|
||||
_settings = new JsonSerializerSettings
|
||||
{
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
};
|
||||
_serialized = JsonConvert.SerializeObject(order, _settings);
|
||||
_serializedUtf8 = Utf8NoBom.GetBytes(_serialized);
|
||||
}
|
||||
|
||||
public void Warmup(int iterations)
|
||||
{
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
Serialize();
|
||||
Deserialize();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public void Serialize() => JsonConvert.SerializeObject(_order, _settings);
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public void Deserialize() => JsonConvert.DeserializeObject<TestOrder>(_serialized, _settings);
|
||||
}
|
||||
|
||||
private sealed class SystemTextJsonBenchmark : ISerializerBenchmark
|
||||
{
|
||||
private readonly TestOrder _order;
|
||||
private readonly JsonSerializerOptions _options;
|
||||
private readonly string _serialized;
|
||||
private readonly byte[] _serializedUtf8;
|
||||
|
||||
public string Name { get; }
|
||||
public int SerializedSize => _serializedUtf8.Length;
|
||||
|
||||
public SystemTextJsonBenchmark(TestOrder order, string name)
|
||||
{
|
||||
_order = order;
|
||||
Name = name;
|
||||
_options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
||||
ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles
|
||||
};
|
||||
_serialized = System.Text.Json.JsonSerializer.Serialize(order, _options);
|
||||
_serializedUtf8 = Utf8NoBom.GetBytes(_serialized);
|
||||
}
|
||||
|
||||
public void Warmup(int iterations)
|
||||
{
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
Serialize();
|
||||
Deserialize();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public void Serialize() => System.Text.Json.JsonSerializer.Serialize(_order, _options);
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public void Deserialize() => System.Text.Json.JsonSerializer.Deserialize<TestOrder>(_serialized, _options);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Results
|
||||
|
||||
private sealed class TestDataSet
|
||||
{
|
||||
public string Name { get; }
|
||||
public TestOrder Order { get; }
|
||||
|
||||
public TestDataSet(string name, TestOrder order)
|
||||
{
|
||||
Name = name;
|
||||
Order = order;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BenchmarkResult
|
||||
{
|
||||
public string TestDataName { get; set; } = "";
|
||||
public string SerializerName { get; set; } = "";
|
||||
public int SerializedSize { get; set; }
|
||||
public double SerializeTimeMs { get; set; }
|
||||
public double DeserializeTimeMs { get; set; }
|
||||
public double RoundTripTimeMs => SerializeTimeMs + DeserializeTimeMs;
|
||||
}
|
||||
|
||||
private static void PrintResult(BenchmarkResult result)
|
||||
{
|
||||
var ser = result.SerializeTimeMs > 0 ? $"{result.SerializeTimeMs,8:F2} ms" : " N/A";
|
||||
var des = result.DeserializeTimeMs > 0 ? $"{result.DeserializeTimeMs,8:F2} ms" : " N/A";
|
||||
System.Console.WriteLine($" {result.SerializerName,-25} | Size: {result.SerializedSize,8:N0} | Ser: {ser} | Des: {des}");
|
||||
}
|
||||
|
||||
private static void PrintGroupedResults(List<BenchmarkResult> results, List<TestDataSet> testDataSets)
|
||||
{
|
||||
System.Console.WriteLine("\n");
|
||||
System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗");
|
||||
System.Console.WriteLine("║ GROUPED RESULTS BY TEST DATA ║");
|
||||
System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
|
||||
|
||||
foreach (var testData in testDataSets)
|
||||
{
|
||||
var testResults = results.Where(r => r.TestDataName == testData.Name).OrderBy(r => r.RoundTripTimeMs).ToList();
|
||||
var msgPackResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerMessagePack);
|
||||
var acBinaryResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerAcBinaryDefault);
|
||||
|
||||
System.Console.WriteLine($"\n┌─ {testData.Name} ─".PadRight(98, '─') + "┐");
|
||||
System.Console.WriteLine($"│ {"#",-4} │ {"Serializer",-25} │ {"Size",-10} │ {"Serialize",-12} │ {"Deserialize",-12} │ {"Round-trip",-12} │");
|
||||
System.Console.WriteLine($"├{"─".PadRight(6, '─')}┼{"─".PadRight(27, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(14, '─')}┤");
|
||||
|
||||
var rank = 1;
|
||||
foreach (var result in testResults)
|
||||
{
|
||||
var size = $"{result.SerializedSize:N0}";
|
||||
var ser = result.SerializeTimeMs > 0 ? $"{result.SerializeTimeMs:F2} ms" : "N/A";
|
||||
var des = result.DeserializeTimeMs > 0 ? $"{result.DeserializeTimeMs:F2} ms" : "N/A";
|
||||
var rt = result.RoundTripTimeMs > 0 ? $"{result.RoundTripTimeMs:F2} ms" : "N/A";
|
||||
|
||||
// Highlight MessagePack and AcBinary (Default) with win/lose colors
|
||||
var isHighlighted = result.SerializerName is SerializerMessagePack or SerializerAcBinaryDefault;
|
||||
var prefix = isHighlighted ? "│►" : "│ ";
|
||||
var suffix = isHighlighted ? "◄│" : " │";
|
||||
|
||||
// Color logic: Green = winner (faster), Red = loser (slower)
|
||||
if (isHighlighted && msgPackResult != null && acBinaryResult != null)
|
||||
{
|
||||
var isMsgPack = result.SerializerName == SerializerMessagePack;
|
||||
var msgPackFaster = msgPackResult.RoundTripTimeMs < acBinaryResult.RoundTripTimeMs;
|
||||
|
||||
if (isMsgPack)
|
||||
{
|
||||
System.Console.ForegroundColor = msgPackFaster ? ConsoleColor.Green : ConsoleColor.Red;
|
||||
}
|
||||
else
|
||||
{
|
||||
System.Console.ForegroundColor = msgPackFaster ? ConsoleColor.Red : ConsoleColor.Green;
|
||||
}
|
||||
}
|
||||
|
||||
System.Console.WriteLine($"{prefix}{rank++,4} │ {result.SerializerName,-25} │ {size,10} │ {ser,12} │ {des,12} │ {rt,12}{suffix}");
|
||||
|
||||
if (isHighlighted)
|
||||
{
|
||||
System.Console.ResetColor();
|
||||
}
|
||||
}
|
||||
|
||||
// Footer row: AcBinary (Default) vs MessagePack comparison per column
|
||||
if (msgPackResult != null && acBinaryResult != null)
|
||||
{
|
||||
var sizePct = (acBinaryResult.SerializedSize / (double)msgPackResult.SerializedSize - 1) * 100;
|
||||
var serPct = msgPackResult.SerializeTimeMs > 0 ? (acBinaryResult.SerializeTimeMs / msgPackResult.SerializeTimeMs - 1) * 100 : 0;
|
||||
var desPct = msgPackResult.DeserializeTimeMs > 0 ? (acBinaryResult.DeserializeTimeMs / msgPackResult.DeserializeTimeMs - 1) * 100 : 0;
|
||||
var rtPct = msgPackResult.RoundTripTimeMs > 0 ? (acBinaryResult.RoundTripTimeMs / msgPackResult.RoundTripTimeMs - 1) * 100 : 0;
|
||||
|
||||
System.Console.WriteLine($"├{"─".PadRight(6, '─')}┴{"─".PadRight(27, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(13, '─')}┤");
|
||||
System.Console.Write($"│ ► Default vs {SerializerMessagePack,-19} │ ");
|
||||
|
||||
// Size
|
||||
System.Console.ForegroundColor = sizePct <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||||
System.Console.Write($"{sizePct,+9:+0;-0}%");
|
||||
System.Console.ResetColor();
|
||||
System.Console.Write(" │ ");
|
||||
|
||||
// Serialize
|
||||
System.Console.ForegroundColor = serPct <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||||
System.Console.Write($"{serPct,+11:+0;-0}%");
|
||||
System.Console.ResetColor();
|
||||
System.Console.Write(" │ ");
|
||||
|
||||
// Deserialize
|
||||
System.Console.ForegroundColor = desPct <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||||
System.Console.Write($"{desPct,+11:+0;-0}%");
|
||||
System.Console.ResetColor();
|
||||
System.Console.Write(" │ ");
|
||||
|
||||
// Round-trip
|
||||
System.Console.ForegroundColor = rtPct <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||||
System.Console.Write($"{rtPct,+10:+0;-0}%");
|
||||
System.Console.ResetColor();
|
||||
System.Console.WriteLine(" │");
|
||||
}
|
||||
|
||||
System.Console.WriteLine($"└{"─".PadRight(6, '─')}─{"─".PadRight(27, '─')}┴{"─".PadRight(12, '─')}┴{"─".PadRight(14, '─')}┴{"─".PadRight(14, '─')}┴{"─".PadRight(13, '─')}┘");
|
||||
}
|
||||
|
||||
// Summary: Best serializer for each category
|
||||
System.Console.WriteLine("\n");
|
||||
System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗");
|
||||
System.Console.WriteLine("║ SUMMARY: WINNERS ║");
|
||||
System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
|
||||
|
||||
System.Console.WriteLine($"\n{"Category",-20} │ {"Winner",-25} │ {"Avg Value",-18}");
|
||||
System.Console.WriteLine($"{"─".PadRight(20, '─')}─┼─{"─".PadRight(25, '─')}─┼─{"─".PadRight(18, '─')}");
|
||||
|
||||
// Fastest Serialize
|
||||
var fastestSer = results.Where(r => r.SerializeTimeMs > 0)
|
||||
.GroupBy(r => r.SerializerName)
|
||||
.Select(g => new { Name = g.Key, AvgTime = g.Average(r => r.SerializeTimeMs) })
|
||||
.OrderBy(x => x.AvgTime)
|
||||
.FirstOrDefault();
|
||||
if (fastestSer != null)
|
||||
System.Console.WriteLine($"{"Fastest Serialize",-20} │ {fastestSer.Name,-25} │ {fastestSer.AvgTime,15:F2} ms");
|
||||
|
||||
// Fastest Deserialize
|
||||
var fastestDes = results.Where(r => r.DeserializeTimeMs > 0)
|
||||
.GroupBy(r => r.SerializerName)
|
||||
.Select(g => new { Name = g.Key, AvgTime = g.Average(r => r.DeserializeTimeMs) })
|
||||
.OrderBy(x => x.AvgTime)
|
||||
.FirstOrDefault();
|
||||
if (fastestDes != null)
|
||||
System.Console.WriteLine($"{"Fastest Deserialize",-20} │ {fastestDes.Name,-25} │ {fastestDes.AvgTime,15:F2} ms");
|
||||
|
||||
// Smallest Size
|
||||
var smallestSize = results
|
||||
.GroupBy(r => r.SerializerName)
|
||||
.Select(g => new { Name = g.Key, AvgSize = g.Average(r => r.SerializedSize) })
|
||||
.OrderBy(x => x.AvgSize)
|
||||
.FirstOrDefault();
|
||||
if (smallestSize != null)
|
||||
System.Console.WriteLine($"{"Smallest Size",-20} │ {smallestSize.Name,-25} │ {smallestSize.AvgSize,15:F0} B");
|
||||
|
||||
// Fastest Round-trip
|
||||
var fastestRt = results.Where(r => r.RoundTripTimeMs > 0)
|
||||
.GroupBy(r => r.SerializerName)
|
||||
.Select(g => new { Name = g.Key, AvgTime = g.Average(r => r.RoundTripTimeMs) })
|
||||
.OrderBy(x => x.AvgTime)
|
||||
.FirstOrDefault();
|
||||
if (fastestRt != null)
|
||||
System.Console.WriteLine($"{"Fastest Round-trip",-20} │ {fastestRt.Name,-25} │ {fastestRt.AvgTime,15:F2} ms");
|
||||
|
||||
// Overall AcBinary Default vs MessagePack comparison
|
||||
var msgPackAvgSer = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).Average(r => r.SerializeTimeMs);
|
||||
var msgPackAvgDes = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs);
|
||||
var msgPackAvgRt = results.Where(r => r.SerializerName == SerializerMessagePack && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs);
|
||||
var msgPackAvgSize = results.Where(r => r.SerializerName == SerializerMessagePack).Average(r => r.SerializedSize);
|
||||
|
||||
var acBinaryAvgSer = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.SerializeTimeMs > 0).Average(r => r.SerializeTimeMs);
|
||||
var acBinaryAvgDes = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs);
|
||||
var acBinaryAvgRt = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs);
|
||||
var acBinaryAvgSize = results.Where(r => r.SerializerName == SerializerAcBinaryDefault).Average(r => r.SerializedSize);
|
||||
|
||||
System.Console.WriteLine();
|
||||
System.Console.WriteLine($"── {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ──");
|
||||
|
||||
var serPctAll = (acBinaryAvgSer / msgPackAvgSer - 1) * 100;
|
||||
var desPctAll = (acBinaryAvgDes / msgPackAvgDes - 1) * 100;
|
||||
var rtPctAll = (acBinaryAvgRt / msgPackAvgRt - 1) * 100;
|
||||
var sizePctAll = (acBinaryAvgSize / msgPackAvgSize - 1) * 100;
|
||||
|
||||
System.Console.ForegroundColor = serPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||||
System.Console.WriteLine($" Serialize: {serPctAll:+0;-0}% ({acBinaryAvgSer:F2} ms vs {msgPackAvgSer:F2} ms)");
|
||||
System.Console.ResetColor();
|
||||
|
||||
System.Console.ForegroundColor = desPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||||
System.Console.WriteLine($" Deserialize: {desPctAll:+0;-0}% ({acBinaryAvgDes:F2} ms vs {msgPackAvgDes:F2} ms)");
|
||||
System.Console.ResetColor();
|
||||
|
||||
System.Console.ForegroundColor = rtPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||||
System.Console.WriteLine($" Round-trip: {rtPctAll:+0;-0}% ({acBinaryAvgRt:F2} ms vs {msgPackAvgRt:F2} ms)");
|
||||
System.Console.ResetColor();
|
||||
|
||||
System.Console.ForegroundColor = sizePctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||||
System.Console.WriteLine($" Size: {sizePctAll:+0;-0}% ({acBinaryAvgSize:F0} B vs {msgPackAvgSize:F0} B)");
|
||||
System.Console.ResetColor();
|
||||
}
|
||||
|
||||
private static void SaveResults(List<BenchmarkResult> results, List<TestDataSet> testDataSets)
|
||||
{
|
||||
Directory.CreateDirectory(ResultsDirectory);
|
||||
|
||||
var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
|
||||
var fileName = $"Console.FullBenchmark_{BuildConfiguration}_{timestamp}.log";
|
||||
var filePath = Path.Combine(ResultsDirectory, fileName);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗");
|
||||
sb.AppendLine("║ SERIALIZER BENCHMARK RESULTS ║");
|
||||
sb.AppendLine($"║ Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}".PadRight(100) + "║");
|
||||
sb.AppendLine($"║ Build: {BuildConfiguration}".PadRight(100) + "║");
|
||||
sb.AppendLine($"║ Iterations: {TestIterations}".PadRight(100) + "║");
|
||||
sb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
|
||||
sb.AppendLine();
|
||||
|
||||
// CSV-like data for easy import
|
||||
sb.AppendLine("=== RAW DATA (CSV) ===");
|
||||
sb.AppendLine("TestData,Serializer,Size,SerializeMs,DeserializeMs,RoundTripMs");
|
||||
foreach (var testData in testDataSets)
|
||||
{
|
||||
var testResults = results.Where(r => r.TestDataName == testData.Name).ToList();
|
||||
foreach (var result in testResults)
|
||||
{
|
||||
sb.AppendLine($"{result.TestDataName},{result.SerializerName},{result.SerializedSize},{result.SerializeTimeMs:F2},{result.DeserializeTimeMs:F2},{result.RoundTripTimeMs:F2}");
|
||||
}
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
// Formatted results
|
||||
sb.AppendLine("=== FORMATTED RESULTS BY TEST DATA ===");
|
||||
sb.AppendLine($"(►) = Highlighted: {SerializerMessagePack} (baseline) and {SerializerAcBinaryDefault}");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var testData in testDataSets)
|
||||
{
|
||||
var testResults = results.Where(r => r.TestDataName == testData.Name).OrderBy(r => r.RoundTripTimeMs).ToList();
|
||||
var msgPackResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerMessagePack);
|
||||
var acBinaryResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerAcBinaryDefault);
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"--- {testData.Name} ---");
|
||||
sb.AppendLine($"{"#",-4} {"Serializer",-26} {"Size",-12} {"Serialize",-14} {"Deserialize",-14} {"Round-trip",-14}");
|
||||
sb.AppendLine(new string('-', 86));
|
||||
|
||||
var rank = 1;
|
||||
foreach (var result in testResults)
|
||||
{
|
||||
var isHighlighted = result.SerializerName is SerializerMessagePack or SerializerAcBinaryDefault;
|
||||
var prefix = isHighlighted ? "► " : " ";
|
||||
|
||||
var size = $"{result.SerializedSize:N0}";
|
||||
var ser = result.SerializeTimeMs > 0 ? $"{result.SerializeTimeMs:F2} ms" : "N/A";
|
||||
var des = result.DeserializeTimeMs > 0 ? $"{result.DeserializeTimeMs:F2} ms" : "N/A";
|
||||
var rt = result.RoundTripTimeMs > 0 ? $"{result.RoundTripTimeMs:F2} ms" : "N/A";
|
||||
|
||||
sb.AppendLine($"{rank++,2} {prefix}{result.SerializerName,-24} {size,-12} {ser,-14} {des,-14} {rt,-14}");
|
||||
}
|
||||
|
||||
// Summary row for this test data
|
||||
if (msgPackResult != null && acBinaryResult != null)
|
||||
{
|
||||
var sizePct = (acBinaryResult.SerializedSize / (double)msgPackResult.SerializedSize - 1) * 100;
|
||||
var serPct = msgPackResult.SerializeTimeMs > 0 ? (acBinaryResult.SerializeTimeMs / msgPackResult.SerializeTimeMs - 1) * 100 : 0;
|
||||
var desPct = msgPackResult.DeserializeTimeMs > 0 ? (acBinaryResult.DeserializeTimeMs / msgPackResult.DeserializeTimeMs - 1) * 100 : 0;
|
||||
var rtPct = msgPackResult.RoundTripTimeMs > 0 ? (acBinaryResult.RoundTripTimeMs / msgPackResult.RoundTripTimeMs - 1) * 100 : 0;
|
||||
|
||||
sb.AppendLine($" {SerializerAcBinaryDefault} vs {SerializerMessagePack}: Size {sizePct:+0;-0}% │ Ser {serPct:+0;-0}% │ Des {desPct:+0;-0}% │ RT {rtPct:+0;-0}%");
|
||||
}
|
||||
}
|
||||
|
||||
// Summary comparison
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"=== {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ===");
|
||||
|
||||
var msgPackAvgSer = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).Average(r => r.SerializeTimeMs);
|
||||
var msgPackAvgDes = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs);
|
||||
var msgPackAvgRt = results.Where(r => r.SerializerName == SerializerMessagePack && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs);
|
||||
var msgPackAvgSize = results.Where(r => r.SerializerName == SerializerMessagePack).Average(r => r.SerializedSize);
|
||||
|
||||
var acBinaryAvgSer = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.SerializeTimeMs > 0).Average(r => r.SerializeTimeMs);
|
||||
var acBinaryAvgDes = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.DeserializeTimeMs > 0).Average(r => r.DeserializeTimeMs);
|
||||
var acBinaryAvgRt = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.RoundTripTimeMs > 0).Average(r => r.RoundTripTimeMs);
|
||||
var acBinaryAvgSize = results.Where(r => r.SerializerName == SerializerAcBinaryDefault).Average(r => r.SerializedSize);
|
||||
|
||||
sb.AppendLine($" Serialize: {((acBinaryAvgSer / msgPackAvgSer - 1) * 100):+0;-0}% ({acBinaryAvgSer:F2} ms vs {msgPackAvgSer:F2} ms)");
|
||||
sb.AppendLine($" Deserialize: {((acBinaryAvgDes / msgPackAvgDes - 1) * 100):+0;-0}% ({acBinaryAvgDes:F2} ms vs {msgPackAvgDes:F2} ms)");
|
||||
sb.AppendLine($" Round-trip: {((acBinaryAvgRt / msgPackAvgRt - 1) * 100):+0;-0}% ({acBinaryAvgRt:F2} ms vs {msgPackAvgRt:F2} ms)");
|
||||
sb.AppendLine($" Size: {((acBinaryAvgSize / msgPackAvgSize - 1) * 100):+0;-0}% ({acBinaryAvgSize:F0} B vs {msgPackAvgSize:F0} B)");
|
||||
|
||||
File.WriteAllText(filePath, sb.ToString(), Utf8NoBom);
|
||||
System.Console.WriteLine($"\n✓ Results saved to: {filePath}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ public class AcBinarySerializerNavigationPropertyTests
|
|||
Assert.AreEqual("SharedProduct", result.Items[1].Product.Name);
|
||||
|
||||
// With reference handling, they should be the same instance
|
||||
// (This depends on UseReferenceHandling being enabled)
|
||||
// (This depends on ReferenceHandling being enabled)
|
||||
Console.WriteLine($"Same instance: {ReferenceEquals(result.Items[0].Product, result.Items[1].Product)}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -781,7 +781,7 @@ public static class AcSerializerCommon
|
|||
|
||||
/// <summary>
|
||||
/// Common reference tracking for serialization.
|
||||
/// Uses unified Bloom filter + HashSet for both IId and Reference tracking.
|
||||
/// Uses HashSet for tracking seen objects.
|
||||
/// - IId objects: hash = Id (positive, DB guarantees uniqueness per entity type)
|
||||
/// - Non-IId objects: hash = RuntimeHelpers.GetHashCode | 0x80000000 (negative)
|
||||
/// </summary>
|
||||
|
|
@ -789,9 +789,6 @@ public static class AcSerializerCommon
|
|||
{
|
||||
private const int InitialCapacity = 128;
|
||||
|
||||
// Unified Bloom filter (256 bits = 4 x 64-bit)
|
||||
private ulong _bloom0, _bloom1, _bloom2, _bloom3;
|
||||
|
||||
// Unified HashSet for seen hashes (both IId and Reference)
|
||||
private HashSet<int>? _seenHashes;
|
||||
|
||||
|
|
@ -809,7 +806,6 @@ public static class AcSerializerCommon
|
|||
public void Reset()
|
||||
{
|
||||
_nextRefId = 1;
|
||||
_bloom0 = _bloom1 = _bloom2 = _bloom3 = 0;
|
||||
_seenHashes?.Clear();
|
||||
_multiRefHashes?.Clear();
|
||||
_writtenRefs?.Clear();
|
||||
|
|
@ -878,41 +874,14 @@ public static class AcSerializerCommon
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core tracking logic using Bloom filter + HashSet.
|
||||
/// Core tracking logic using HashSet.Add() which returns false if already exists.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool TrackHash(int hash)
|
||||
{
|
||||
// Bloom filter check - fast "definitely new" detection
|
||||
var segment = (hash >> 6) & 3;
|
||||
var bit = hash & 63;
|
||||
var mask = 1UL << bit;
|
||||
|
||||
var bloomHit = segment switch
|
||||
{
|
||||
0 => (_bloom0 & mask) != 0,
|
||||
1 => (_bloom1 & mask) != 0,
|
||||
2 => (_bloom2 & mask) != 0,
|
||||
_ => (_bloom3 & mask) != 0
|
||||
};
|
||||
|
||||
if (!bloomHit)
|
||||
{
|
||||
// Definitely new - add to bloom and set
|
||||
switch (segment)
|
||||
{
|
||||
case 0: _bloom0 |= mask; break;
|
||||
case 1: _bloom1 |= mask; break;
|
||||
case 2: _bloom2 |= mask; break;
|
||||
default: _bloom3 |= mask; break;
|
||||
}
|
||||
_seenHashes ??= new HashSet<int>(InitialCapacity);
|
||||
_seenHashes.Add(hash);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Possible duplicate - check HashSet
|
||||
_seenHashes ??= new HashSet<int>(InitialCapacity);
|
||||
// HashSet.Add returns false if element already exists
|
||||
if (!_seenHashes.Add(hash))
|
||||
{
|
||||
// Already seen - multi-referenced!
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using AyCode.Core.Serializers.Jsons;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
|
@ -14,6 +15,8 @@ namespace AyCode.Core.Serializers;
|
|||
/// <typeparam name="TMetadata">The concrete metadata type.</typeparam>
|
||||
public abstract class AcSerializerContextBase<TMetadata> where TMetadata : TypeMetadataBase
|
||||
{
|
||||
public byte MaxDepth { get; private set; }
|
||||
public ReferenceHandlingMode ReferenceHandling { get; internal set; }
|
||||
/// <summary>
|
||||
/// Global shared cache for metadata (thread-safe, shared across all contexts).
|
||||
/// Generic specialization ensures separate cache per TMetadata type.
|
||||
|
|
@ -116,12 +119,17 @@ public abstract class AcSerializerContextBase<TMetadata> where TMetadata : TypeM
|
|||
/// Resets all wrapper tracking states for reuse.
|
||||
/// Does not remove wrappers - keeps them for next operation.
|
||||
/// </summary>
|
||||
public virtual void Reset()
|
||||
public virtual void Reset(in AcSerializerOptions? options)
|
||||
{
|
||||
foreach (var wrapper in _wrappers.Values)
|
||||
{
|
||||
wrapper.ResetTracking();
|
||||
}
|
||||
|
||||
if (options == null) return;
|
||||
|
||||
MaxDepth = options.MaxDepth;
|
||||
ReferenceHandling = options.ReferenceHandling;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
|
@ -32,7 +33,10 @@ public static partial class AcBinaryDeserializer
|
|||
public readonly BinaryDeserializationContextClass ContextClass;
|
||||
|
||||
public bool HasMetadata { get; private set; }
|
||||
public bool HasReferenceHandling { get; private set; }
|
||||
/// <summary>
|
||||
/// Convenience property - true if any reference handling is enabled.
|
||||
/// </summary>
|
||||
public bool HasReferenceHandling => ContextClass?.ReferenceHandling != ReferenceHandlingMode.None;
|
||||
public bool IsMergeMode { readonly get; set; }
|
||||
public bool RemoveOrphanedItems { readonly get; set; }
|
||||
public bool IsAtEnd => _position >= _buffer.Length;
|
||||
|
|
@ -69,7 +73,6 @@ public static partial class AcBinaryDeserializer
|
|||
//_objectReferences = null;
|
||||
_stringCache = null;
|
||||
HasMetadata = false;
|
||||
HasReferenceHandling = false;
|
||||
IsMergeMode = false;
|
||||
RemoveOrphanedItems = false;
|
||||
ChainTracker = null;
|
||||
|
|
@ -77,6 +80,8 @@ public static partial class AcBinaryDeserializer
|
|||
_useStringCaching = options.UseStringCaching;
|
||||
_maxCachedStringLength = options.MaxCachedStringLength;
|
||||
ContextClass = contextClass;
|
||||
// Initialize ReferenceHandling from options (will be overwritten by ReadHeader if present in stream)
|
||||
ContextClass.ReferenceHandling = options.ReferenceHandling;
|
||||
}
|
||||
|
||||
public void ReadHeader()
|
||||
|
|
@ -97,22 +102,38 @@ public static partial class AcBinaryDeserializer
|
|||
var marker = ReadByteInternal();
|
||||
var hasPropertyTable = false;
|
||||
var hasInternTable = false;
|
||||
var hasInternFooter = false;
|
||||
var footerPosition = 0;
|
||||
|
||||
if (marker == BinaryTypeCode.MetadataHeader)
|
||||
{
|
||||
hasPropertyTable = true;
|
||||
HasReferenceHandling = true;
|
||||
ContextClass.ReferenceHandling = ReferenceHandlingMode.OnlyId; // Legacy: assume OnlyId
|
||||
}
|
||||
else if (marker == BinaryTypeCode.NoMetadataHeader)
|
||||
{
|
||||
HasReferenceHandling = true;
|
||||
ContextClass.ReferenceHandling = ReferenceHandlingMode.OnlyId; // Legacy: assume OnlyId
|
||||
}
|
||||
else if ((marker & 0xF0) == BinaryTypeCode.HeaderFlagsBase)
|
||||
{
|
||||
var flags = (byte)(marker & 0x0F);
|
||||
hasPropertyTable = (flags & BinaryTypeCode.HeaderFlag_Metadata) != 0;
|
||||
HasReferenceHandling = (flags & BinaryTypeCode.HeaderFlag_ReferenceHandling) != 0;
|
||||
hasInternTable = (flags & BinaryTypeCode.HeaderFlag_StringInternTable) != 0;
|
||||
// Decode ReferenceHandlingMode from separate bits
|
||||
var hasOnlyId = (flags & BinaryTypeCode.HeaderFlag_RefHandling_OnlyId) != 0;
|
||||
var hasAll = (flags & BinaryTypeCode.HeaderFlag_RefHandling_All) != 0;
|
||||
ContextClass.ReferenceHandling = hasAll ? ReferenceHandlingMode.All
|
||||
: hasOnlyId ? ReferenceHandlingMode.OnlyId
|
||||
: ReferenceHandlingMode.None;
|
||||
|
||||
// Read footer position if flag is set
|
||||
var hasFooterPosition = (flags & BinaryTypeCode.HeaderFlag_HasFooterPosition) != 0;
|
||||
if (hasFooterPosition)
|
||||
{
|
||||
EnsureAvailable(4);
|
||||
footerPosition = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position, 4));
|
||||
_position += 4;
|
||||
hasInternFooter = footerPosition > 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -133,6 +154,7 @@ public static partial class AcBinaryDeserializer
|
|||
}
|
||||
}
|
||||
|
||||
// Legacy: interned strings in header
|
||||
if (hasInternTable)
|
||||
{
|
||||
var internCount = (int)ReadVarUInt();
|
||||
|
|
@ -142,6 +164,36 @@ public static partial class AcBinaryDeserializer
|
|||
_internedStrings.Add(ReadHeaderString());
|
||||
}
|
||||
}
|
||||
|
||||
// Footer-based: read interned strings from footer, then return to data position
|
||||
if (hasInternFooter && footerPosition > 0)
|
||||
{
|
||||
ReadFooterStrings(footerPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads interned strings from footer position, then returns to data position.
|
||||
/// Uses seek to footer, read strings, seek back to data.
|
||||
/// </summary>
|
||||
private void ReadFooterStrings(int footerPosition)
|
||||
{
|
||||
// Save current position (start of data)
|
||||
var dataPosition = _position;
|
||||
|
||||
// Seek to footer
|
||||
_position = footerPosition;
|
||||
|
||||
// Read interned strings
|
||||
var internCount = (int)ReadVarUInt();
|
||||
_internedStrings = new List<string>(internCount);
|
||||
for (var i = 0; i < internCount; i++)
|
||||
{
|
||||
_internedStrings.Add(ReadHeaderString());
|
||||
}
|
||||
|
||||
// Seek back to data position
|
||||
_position = dataPosition;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using AyCode.Core.Serializers.Jsons;
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
|
|
@ -56,10 +57,6 @@ public static partial class AcBinarySerializer
|
|||
private const int InitialInternCapacity = 32;
|
||||
private const int InitialPropertyNameCapacity = 32;
|
||||
|
||||
// Bloom filter constants for string interning
|
||||
private const int BloomFilterSize = 256; // 256 bits = 32 bytes
|
||||
private const int BloomFilterMask = BloomFilterSize - 1;
|
||||
|
||||
private byte[] _buffer;
|
||||
private int _position;
|
||||
private int _initialBufferSize;
|
||||
|
|
@ -70,24 +67,13 @@ public static partial class AcBinarySerializer
|
|||
private Dictionary<string, int>? _internedStrings;
|
||||
private List<string>? _internedStringList;
|
||||
|
||||
/// <summary>
|
||||
/// Bloom filter for quick "definitely not interned" checks.
|
||||
/// Avoids dictionary lookup for unique strings.
|
||||
/// </summary>
|
||||
private ulong _bloomFilter0;
|
||||
private ulong _bloomFilter1;
|
||||
private ulong _bloomFilter2;
|
||||
private ulong _bloomFilter3;
|
||||
|
||||
private Dictionary<string, int>? _propertyNames;
|
||||
private List<string>? _propertyNameList;
|
||||
private int[]? _propertyIndexBuffer;
|
||||
private byte[]? _propertyStateBuffer;
|
||||
|
||||
public bool UseReferenceHandling { get; private set; }
|
||||
public bool UseStringInterning { get; private set; }
|
||||
public bool UseMetadata { get; private set; }
|
||||
public byte MaxDepth { get; private set; }
|
||||
public byte MinStringInternLength { get; private set; }
|
||||
public byte MaxStringInternLength { get; private set; }
|
||||
public BinaryPropertyFilter? PropertyFilter { get; private set; }
|
||||
|
|
@ -109,25 +95,17 @@ public static partial class AcBinarySerializer
|
|||
|
||||
public void Reset(AcBinarySerializerOptions options)
|
||||
{
|
||||
// Reset wrapper tracking state from base class (IId tracking)
|
||||
base.Reset(options);
|
||||
|
||||
_position = 0;
|
||||
UseReferenceHandling = options.UseReferenceHandling;
|
||||
UseStringInterning = options.UseStringInterning;
|
||||
UseMetadata = options.UseMetadata;
|
||||
MaxDepth = options.MaxDepth;
|
||||
MinStringInternLength = options.MinStringInternLength;
|
||||
MaxStringInternLength = options.MaxStringInternLength;
|
||||
PropertyFilter = options.PropertyFilter;
|
||||
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
|
||||
|
||||
//_refTracker.Reset();
|
||||
if (UseReferenceHandling)
|
||||
{
|
||||
//_refTracker.EnsureInitialized();
|
||||
}
|
||||
|
||||
// Reset wrapper tracking state from base class (IId tracking)
|
||||
base.Reset();
|
||||
|
||||
if (_buffer.Length < _initialBufferSize)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(_buffer);
|
||||
|
|
@ -139,12 +117,6 @@ public static partial class AcBinarySerializer
|
|||
{
|
||||
_position = 0;
|
||||
|
||||
// Reset bloom filter
|
||||
_bloomFilter0 = 0;
|
||||
_bloomFilter1 = 0;
|
||||
_bloomFilter2 = 0;
|
||||
_bloomFilter3 = 0;
|
||||
|
||||
//_refTracker.Reset();
|
||||
ClearAndTrimIfNeeded(_internedStrings, InitialInternCapacity * 4);
|
||||
ClearAndTrimIfNeeded(_propertyNames, InitialPropertyNameCapacity * 4);
|
||||
|
|
@ -169,7 +141,7 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
}
|
||||
|
||||
private void ResetCachedPropertyIndices()
|
||||
private static void ResetCachedPropertyIndices()
|
||||
{
|
||||
// Note: BinaryPropertyAccessor.CachedPropertyNameIndex is per-context,
|
||||
// but metadata is cached globally. We reset it during Clear to avoid
|
||||
|
|
@ -205,6 +177,10 @@ public static partial class AcBinarySerializer
|
|||
/// </summary>
|
||||
private List<byte[]>? _internedStringUtf8;
|
||||
|
||||
/// <summary>
|
||||
/// Registers a string for interning. Returns the index of the string.
|
||||
/// Uses CollectionsMarshal.GetValueRefOrAddDefault for single-operation lookup+add.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int RegisterInternedString(string value)
|
||||
{
|
||||
|
|
@ -212,32 +188,17 @@ public static partial class AcBinarySerializer
|
|||
_internedStringList ??= new List<string>(InitialInternCapacity);
|
||||
_internedStringUtf8 ??= new List<byte[]>(InitialInternCapacity);
|
||||
|
||||
// Fast path: check bloom filter first
|
||||
var hash = GetStringHash(value);
|
||||
if (!BloomFilterMightContain(hash))
|
||||
{
|
||||
// Definitely not in dictionary - add directly
|
||||
var newIndex = _internedStringList.Count;
|
||||
_internedStrings[value] = newIndex;
|
||||
_internedStringList.Add(value);
|
||||
// Cache UTF8 bytes immediately
|
||||
_internedStringUtf8.Add(GetUtf8BytesCached(value));
|
||||
BloomFilterAdd(hash);
|
||||
return newIndex;
|
||||
}
|
||||
|
||||
// Might be in dictionary - need to check
|
||||
// Single operation: lookup + conditional add
|
||||
ref var index = ref CollectionsMarshal.GetValueRefOrAddDefault(_internedStrings, value, out var exists);
|
||||
if (exists)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
|
||||
// New string - add to lists
|
||||
index = _internedStringList.Count;
|
||||
_internedStringList.Add(value);
|
||||
// Cache UTF8 bytes immediately
|
||||
_internedStringUtf8.Add(GetUtf8BytesCached(value));
|
||||
BloomFilterAdd(hash);
|
||||
return index;
|
||||
}
|
||||
|
||||
|
|
@ -248,77 +209,16 @@ public static partial class AcBinarySerializer
|
|||
private static byte[] GetUtf8BytesCached(string value)
|
||||
{
|
||||
// Fast path for ASCII strings - direct char to byte conversion
|
||||
if (System.Text.Ascii.IsValid(value))
|
||||
if (Ascii.IsValid(value))
|
||||
{
|
||||
var bytes = new byte[value.Length];
|
||||
System.Text.Ascii.FromUtf16(value.AsSpan(), bytes, out _);
|
||||
Ascii.FromUtf16(value.AsSpan(), bytes, out _);
|
||||
return bytes;
|
||||
}
|
||||
// Standard path for multi-byte UTF8
|
||||
return Utf8NoBom.GetBytes(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int GetStringHash(string value)
|
||||
{
|
||||
// Simple hash combining length and first/last characters
|
||||
// Optimized for quick calculation, not collision resistance
|
||||
if (value.Length == 0) return 0;
|
||||
var h = value.Length;
|
||||
h = (h * 31) + value[0];
|
||||
if (value.Length > 1)
|
||||
h = (h * 31) + value[value.Length - 1];
|
||||
return h;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool BloomFilterMightContain(int hash)
|
||||
{
|
||||
// Use two hash functions for bloom filter
|
||||
var h1 = hash & BloomFilterMask;
|
||||
var h2 = (hash >> 8) & BloomFilterMask;
|
||||
|
||||
return BloomFilterTestBit(h1) && BloomFilterTestBit(h2);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool BloomFilterTestBit(int bit)
|
||||
{
|
||||
var segment = bit >> 6; // Divide by 64
|
||||
var mask = 1UL << (bit & 63);
|
||||
return segment switch
|
||||
{
|
||||
0 => (_bloomFilter0 & mask) != 0,
|
||||
1 => (_bloomFilter1 & mask) != 0,
|
||||
2 => (_bloomFilter2 & mask) != 0,
|
||||
_ => (_bloomFilter3 & mask) != 0,
|
||||
};
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void BloomFilterAdd(int hash)
|
||||
{
|
||||
var h1 = hash & BloomFilterMask;
|
||||
var h2 = (hash >> 8) & BloomFilterMask;
|
||||
|
||||
BloomFilterSetBit(h1);
|
||||
BloomFilterSetBit(h2);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void BloomFilterSetBit(int bit)
|
||||
{
|
||||
var segment = bit >> 6;
|
||||
var mask = 1UL << (bit & 63);
|
||||
switch (segment)
|
||||
{
|
||||
case 0: _bloomFilter0 |= mask; break;
|
||||
case 1: _bloomFilter1 |= mask; break;
|
||||
case 2: _bloomFilter2 |= mask; break;
|
||||
default: _bloomFilter3 |= mask; break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Name Table
|
||||
|
|
@ -746,12 +646,12 @@ public static partial class AcBinarySerializer
|
|||
public void WriteStringUtf8(string value)
|
||||
{
|
||||
// Fast path for ASCII-only strings using SIMD-optimized check
|
||||
if (System.Text.Ascii.IsValid(value))
|
||||
if (Ascii.IsValid(value))
|
||||
{
|
||||
WriteVarUInt((uint)value.Length);
|
||||
EnsureCapacity(value.Length);
|
||||
// Use System.Text.Ascii for SIMD-optimized ASCII to bytes conversion
|
||||
System.Text.Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, value.Length), out _);
|
||||
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, value.Length), out _);
|
||||
_position += value.Length;
|
||||
return;
|
||||
}
|
||||
|
|
@ -971,32 +871,28 @@ public static partial class AcBinarySerializer
|
|||
#region Header and Metadata
|
||||
|
||||
private int _headerPosition;
|
||||
private int _estimatedHeaderSize;
|
||||
|
||||
// Footer-based string interning: no estimation or shifting needed
|
||||
// Header: [version][flags][footerPosition (4 bytes, only if string interning)]
|
||||
// Body: data with StringInterned indices
|
||||
// Footer: interned strings table
|
||||
|
||||
/// <summary>
|
||||
/// Estimates header payload size based on registered property names and intern strings.
|
||||
/// Call after metadata registration but before writing the body.
|
||||
/// Estimates header payload size based on registered property names.
|
||||
/// String interning now uses footer, so no estimation needed for strings.
|
||||
/// </summary>
|
||||
public int EstimateHeaderPayloadSize()
|
||||
{
|
||||
var size = 0;
|
||||
|
||||
// Only property names are in header now
|
||||
if (UseMetadata && _propertyNameList is { Count: > 0 })
|
||||
{
|
||||
size += GetVarUIntSize((uint)_propertyNameList.Count);
|
||||
foreach (var name in _propertyNameList)
|
||||
for (var i = 0; i < _propertyNameList.Count; i++)
|
||||
{
|
||||
var byteCount = name.Length; // Assume ASCII (common case), fallback handles multi-byte
|
||||
size += GetVarUIntSize((uint)byteCount) + byteCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (UseStringInterning && _internedStringList is { Count: > 0 })
|
||||
{
|
||||
size += GetVarUIntSize((uint)_internedStringList.Count);
|
||||
foreach (var value in _internedStringList)
|
||||
{
|
||||
var byteCount = value.Length; // Assume ASCII for estimation
|
||||
var name = _propertyNameList[i];
|
||||
var byteCount = name.Length; // Assume ASCII (common case)
|
||||
size += GetVarUIntSize((uint)byteCount) + byteCount;
|
||||
}
|
||||
}
|
||||
|
|
@ -1006,124 +902,107 @@ public static partial class AcBinarySerializer
|
|||
|
||||
public void WriteHeaderPlaceholder()
|
||||
{
|
||||
EnsureCapacity(2);
|
||||
// Header layout:
|
||||
// [0] version (1 byte)
|
||||
// [1] flags (1 byte)
|
||||
// [2-5] footer position (4 bytes, only if UseStringInterning)
|
||||
EnsureCapacity(UseStringInterning ? 6 : 2);
|
||||
_headerPosition = _position;
|
||||
_position += 2;
|
||||
_estimatedHeaderSize = 0;
|
||||
_position += UseStringInterning ? 6 : 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reserves space for header based on estimation. Call after metadata registration.
|
||||
/// Reserves space for property name table in header.
|
||||
/// </summary>
|
||||
public void ReserveHeaderSpace(int estimatedSize)
|
||||
{
|
||||
if (estimatedSize > 0)
|
||||
{
|
||||
if (estimatedSize <= 0) return;
|
||||
|
||||
EnsureCapacity(estimatedSize);
|
||||
_estimatedHeaderSize = estimatedSize;
|
||||
_position += estimatedSize;
|
||||
}
|
||||
}
|
||||
|
||||
public void FinalizeHeaderSections()
|
||||
{
|
||||
var hasPropertyNames = UseMetadata && _propertyNameList is { Count: > 0 };
|
||||
var hasInternTable = UseStringInterning && _internedStringList is { Count: > 0 };
|
||||
|
||||
// Fast path: no header payload needed
|
||||
if (!hasPropertyNames && !hasInternTable)
|
||||
{
|
||||
// Write header flags only
|
||||
byte flags = BinaryTypeCode.HeaderFlagsBase;
|
||||
if (UseReferenceHandling)
|
||||
flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
|
||||
|
||||
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
|
||||
_buffer[_headerPosition + 1] = flags;
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate actual header size using cached UTF8 bytes
|
||||
var actualSize = 0;
|
||||
// Calculate property names header size (strings go to footer now)
|
||||
var headerPayloadSize = 0;
|
||||
if (hasPropertyNames)
|
||||
{
|
||||
actualSize += GetVarUIntSize((uint)_propertyNameList!.Count);
|
||||
foreach (var name in _propertyNameList)
|
||||
headerPayloadSize += GetVarUIntSize((uint)_propertyNameList!.Count);
|
||||
for (var i = 0; i < _propertyNameList.Count; i++)
|
||||
{
|
||||
var byteCount = System.Text.Ascii.IsValid(name) ? name.Length : Utf8NoBom.GetByteCount(name);
|
||||
actualSize += GetVarUIntSize((uint)byteCount) + byteCount;
|
||||
var name = _propertyNameList[i];
|
||||
var byteCount = Ascii.IsValid(name) ? name.Length : Utf8NoBom.GetByteCount(name);
|
||||
headerPayloadSize += GetVarUIntSize((uint)byteCount) + byteCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasInternTable)
|
||||
{
|
||||
actualSize += GetVarUIntSize((uint)_internedStringList!.Count);
|
||||
// Use cached UTF8 byte lengths
|
||||
for (var i = 0; i < _internedStringUtf8!.Count; i++)
|
||||
{
|
||||
var utf8Bytes = _internedStringUtf8[i];
|
||||
actualSize += GetVarUIntSize((uint)utf8Bytes.Length) + utf8Bytes.Length;
|
||||
}
|
||||
}
|
||||
|
||||
var bodyStart = _headerPosition + 2 + _estimatedHeaderSize;
|
||||
var bodyLength = _position - bodyStart;
|
||||
|
||||
// Shift body if needed
|
||||
if (actualSize != _estimatedHeaderSize && bodyLength > 0)
|
||||
{
|
||||
var delta = actualSize - _estimatedHeaderSize;
|
||||
if (delta > 0)
|
||||
{
|
||||
EnsureCapacity(delta);
|
||||
}
|
||||
|
||||
var newBodyStart = _headerPosition + 2 + actualSize;
|
||||
if (delta != 0)
|
||||
{
|
||||
Array.Copy(_buffer, bodyStart, _buffer, newBodyStart, bodyLength);
|
||||
_position += delta;
|
||||
}
|
||||
}
|
||||
|
||||
// Write header payload directly to buffer using cached UTF8 bytes
|
||||
var headerPos = _headerPosition + 2;
|
||||
|
||||
// Write property names to header if needed
|
||||
var headerPayloadStart = _headerPosition + (UseStringInterning ? 6 : 2);
|
||||
if (hasPropertyNames)
|
||||
{
|
||||
var headerPos = headerPayloadStart;
|
||||
headerPos = WriteVarUIntAt(headerPos, (uint)_propertyNameList!.Count);
|
||||
foreach (var name in _propertyNameList)
|
||||
for (var i = 0; i < _propertyNameList.Count; i++)
|
||||
{
|
||||
var name = _propertyNameList[i];
|
||||
headerPos = WriteStringAtOptimized(headerPos, name);
|
||||
}
|
||||
}
|
||||
|
||||
// Footer-based string interning: write strings at the end
|
||||
var footerPosition = 0;
|
||||
if (hasInternTable)
|
||||
{
|
||||
headerPos = WriteVarUIntAt(headerPos, (uint)_internedStringList!.Count);
|
||||
footerPosition = _position;
|
||||
WriteFooterStrings();
|
||||
}
|
||||
|
||||
// Write header
|
||||
var flags = BinaryTypeCode.HeaderFlagsBase;
|
||||
if (hasPropertyNames)
|
||||
flags |= BinaryTypeCode.HeaderFlag_Metadata;
|
||||
// Encode ReferenceHandlingMode using separate bits
|
||||
if (ReferenceHandling == ReferenceHandlingMode.OnlyId)
|
||||
flags |= BinaryTypeCode.HeaderFlag_RefHandling_OnlyId;
|
||||
else if (ReferenceHandling == ReferenceHandlingMode.All)
|
||||
flags |= (byte)(BinaryTypeCode.HeaderFlag_RefHandling_OnlyId | BinaryTypeCode.HeaderFlag_RefHandling_All);
|
||||
// Set footer position flag if string interning is enabled
|
||||
if (UseStringInterning)
|
||||
flags |= BinaryTypeCode.HeaderFlag_HasFooterPosition;
|
||||
|
||||
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
|
||||
_buffer[_headerPosition + 1] = flags;
|
||||
|
||||
// Always write footer position if string interning is enabled in options
|
||||
// (even if there's no actual interned data - footer position will be 0)
|
||||
if (UseStringInterning)
|
||||
{
|
||||
Unsafe.WriteUnaligned(ref _buffer[_headerPosition + 2], footerPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes interned strings to the footer (end of stream).
|
||||
/// No shifting or estimation needed - just append.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void WriteFooterStrings()
|
||||
{
|
||||
WriteVarUInt((uint)_internedStringList!.Count);
|
||||
|
||||
// Use cached UTF8 bytes - no re-encoding needed!
|
||||
for (var i = 0; i < _internedStringUtf8!.Count; i++)
|
||||
{
|
||||
var utf8Bytes = _internedStringUtf8[i];
|
||||
headerPos = WriteVarUIntAt(headerPos, (uint)utf8Bytes.Length);
|
||||
utf8Bytes.CopyTo(_buffer.AsSpan(headerPos));
|
||||
headerPos += utf8Bytes.Length;
|
||||
WriteVarUInt((uint)utf8Bytes.Length);
|
||||
WriteBytes(utf8Bytes);
|
||||
}
|
||||
}
|
||||
|
||||
// Write header flags
|
||||
byte flags2 = BinaryTypeCode.HeaderFlagsBase;
|
||||
if (hasPropertyNames)
|
||||
flags2 |= BinaryTypeCode.HeaderFlag_Metadata;
|
||||
if (UseReferenceHandling)
|
||||
flags2 |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
|
||||
if (hasInternTable)
|
||||
flags2 |= BinaryTypeCode.HeaderFlag_StringInternTable;
|
||||
|
||||
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
|
||||
_buffer[_headerPosition + 1] = flags2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes UTF8 string at specific position, optimized for ASCII strings.
|
||||
/// </summary>
|
||||
|
|
@ -1131,10 +1010,10 @@ public static partial class AcBinarySerializer
|
|||
private int WriteStringAtOptimized(int pos, string value)
|
||||
{
|
||||
// Fast path for ASCII strings
|
||||
if (System.Text.Ascii.IsValid(value))
|
||||
if (Ascii.IsValid(value))
|
||||
{
|
||||
pos = WriteVarUIntAt(pos, (uint)value.Length);
|
||||
System.Text.Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(pos, value.Length), out _);
|
||||
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(pos, value.Length), out _);
|
||||
return pos + value.Length;
|
||||
}
|
||||
// Standard path for multi-byte UTF8
|
||||
|
|
@ -1173,7 +1052,7 @@ public static partial class AcBinarySerializer
|
|||
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
//public bool TrackForScanningWithIId(object obj, BinarySerializeTypeMetadata metadata, out int existingRefId)
|
||||
//{
|
||||
// if (!UseReferenceHandling)
|
||||
// if (!ReferenceHandling)
|
||||
// {
|
||||
// existingRefId = 0;
|
||||
// return true; // No tracking needed
|
||||
|
|
@ -1239,7 +1118,7 @@ public static partial class AcBinarySerializer
|
|||
var length = value.Length;
|
||||
EnsureCapacity(1 + length);
|
||||
_buffer[_position++] = BinaryTypeCode.EncodeFixStr(length);
|
||||
System.Text.Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, length), out _);
|
||||
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, length), out _);
|
||||
_position += length;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using System.Reflection;
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
using static AyCode.Core.Helpers.JsonUtilities;
|
||||
|
||||
namespace AyCode.Core.Serializers.Binaries;
|
||||
|
|
@ -175,10 +176,12 @@ public static partial class AcBinarySerializer
|
|||
var context = BinarySerializationContextPool.Get(options);
|
||||
context.WriteHeaderPlaceholder();
|
||||
|
||||
// Single-pass serialization - no scan phase needed!
|
||||
// Reference tracking happens inline via TryTrack during WriteObject
|
||||
// Single-pass serialization with footer-based string interning
|
||||
// - No header size estimation needed (strings go to footer)
|
||||
// - No body shifting (footer is appended at the end)
|
||||
// - Reference tracking happens inline via TryTrack during WriteObject
|
||||
|
||||
// Estimate and reserve header space to avoid body shift later
|
||||
// Reserve space only for property name table (if metadata is enabled)
|
||||
var estimatedHeaderSize = context.EstimateHeaderPayloadSize();
|
||||
context.ReserveHeaderSpace(estimatedHeaderSize);
|
||||
|
||||
|
|
@ -639,7 +642,7 @@ public static partial class AcBinarySerializer
|
|||
var metadata = wrapper.Metadata;
|
||||
|
||||
// Single-pass reference tracking
|
||||
if (context.UseReferenceHandling)
|
||||
if (context.ReferenceHandling != ReferenceHandlingMode.None)
|
||||
{
|
||||
switch (metadata.IdAccessorType)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -27,14 +27,13 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
|||
public static readonly AcBinarySerializerOptions Default = new();
|
||||
|
||||
/// <summary>
|
||||
/// Options optimized for maximum speed (no metadata, no interning).
|
||||
/// Options optimized for maximum speed (no interning, no references).
|
||||
/// Use when deserializer knows the exact type structure.
|
||||
/// </summary>
|
||||
public static readonly AcBinarySerializerOptions FastMode = new()
|
||||
{
|
||||
UseMetadata = false,
|
||||
UseStringInterning = false,
|
||||
UseReferenceHandling = false
|
||||
ReferenceHandling = ReferenceHandlingMode.None
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -43,9 +42,8 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
|||
public static readonly AcBinarySerializerOptions ShallowCopy = new()
|
||||
{
|
||||
MaxDepth = 0,
|
||||
UseMetadata = false,
|
||||
UseStringInterning = false,
|
||||
UseReferenceHandling = false
|
||||
ReferenceHandling = ReferenceHandlingMode.None
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -81,11 +79,11 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
|||
|
||||
/// <summary>
|
||||
/// Whether to include metadata header with property names.
|
||||
/// When enabled, property names are stored once and referenced by index.
|
||||
/// Improves deserialization speed and allows schema evolution.
|
||||
/// Default: true
|
||||
/// NOTE: Currently unused - deserializer uses ordered property indices, not names.
|
||||
/// Kept for potential future schema evolution support.
|
||||
/// Default: false (no overhead)
|
||||
/// </summary>
|
||||
public bool UseMetadata { get; init; } = true;
|
||||
public bool UseMetadata { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to intern repeated strings.
|
||||
|
|
@ -140,7 +138,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
|||
/// Creates options without reference handling.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static AcBinarySerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false };
|
||||
public static AcBinarySerializerOptions WithoutReferenceHandling() => new() { ReferenceHandling = ReferenceHandlingMode.None };
|
||||
|
||||
/// <summary>
|
||||
/// Creates options without metadata (faster but less flexible).
|
||||
|
|
@ -213,10 +211,16 @@ internal static class BinaryTypeCode
|
|||
|
||||
// New flag-based header markers (48+) - moved to after FixStr range
|
||||
// Note: FixStr range 34-65 overlaps with old HeaderFlagsBase, so headers use version byte prefix
|
||||
// Header byte structure: (marker & 0xF0) == HeaderFlagsBase, flags in (marker & 0x0F)
|
||||
public const byte HeaderFlagsBase = 48; // Base value for flag-based headers (0x30)
|
||||
public const byte HeaderFlag_Metadata = 0x01;
|
||||
public const byte HeaderFlag_ReferenceHandling = 0x02;
|
||||
public const byte HeaderFlag_StringInternTable = 0x04; // String intern pool preloaded in header
|
||||
public const byte HeaderFlag_Metadata = 0x01; // Bit 0: property metadata included
|
||||
// Reference handling uses 2 separate bits:
|
||||
// Bit 1 (0x02): OnlyId - reference handling for IId objects only
|
||||
// Bit 2 (0x04): All - reference handling for all objects (includes OnlyId)
|
||||
// None = both false, OnlyId = 0x02, All = 0x06 (both bits set)
|
||||
public const byte HeaderFlag_RefHandling_OnlyId = 0x02;
|
||||
public const byte HeaderFlag_RefHandling_All = 0x04;
|
||||
public const byte HeaderFlag_HasFooterPosition = 0x08; // Bit 3: 4-byte footer position follows flags
|
||||
|
||||
// Compact integer variants (for VarInt optimization)
|
||||
public const byte Int32Tiny = 192; // -16 to 63 stored in single byte (value = code - 192 - 16)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
|
||||
namespace AyCode.Core.Serializers;
|
||||
|
||||
|
|
@ -40,9 +41,9 @@ public abstract class DeserializationContextBase<TMetadata> : AcSerializerContex
|
|||
/// <summary>
|
||||
/// Resets deserialization-specific state. Called by derived classes.
|
||||
/// </summary>
|
||||
public override void Reset()
|
||||
public override void Reset(in AcSerializerOptions options)
|
||||
{
|
||||
base.Reset();
|
||||
base.Reset(options);
|
||||
// Future: Reset deserialization-specific state
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,11 +22,11 @@ public static partial class AcJsonDeserializer
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Return(DeserializationContext context)
|
||||
public static void Return(DeserializationContext context, in AcSerializerOptions options)
|
||||
{
|
||||
if (Pool.Count < MaxPoolSize)
|
||||
{
|
||||
context.Clear();
|
||||
context.Clear(null);
|
||||
Pool.Enqueue(context);
|
||||
}
|
||||
}
|
||||
|
|
@ -52,8 +52,6 @@ public static partial class AcJsonDeserializer
|
|||
private List<PropertyToResolve>? _propertiesToResolve;
|
||||
|
||||
public bool IsMergeMode { get; set; }
|
||||
public bool UseReferenceHandling { get; private set; }
|
||||
public byte MaxDepth { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Chain reference tracker for maintaining object identity across chain operations.
|
||||
|
|
@ -77,27 +75,28 @@ public static partial class AcJsonDeserializer
|
|||
Reset(options);
|
||||
}
|
||||
|
||||
public new void Reset(in AcJsonSerializerOptions options)
|
||||
public override void Reset(in AcSerializerOptions? options)
|
||||
{
|
||||
UseReferenceHandling = options.UseReferenceHandling;
|
||||
MaxDepth = options.MaxDepth;
|
||||
IsMergeMode = false;
|
||||
ChainTracker = null;
|
||||
_refTracker.Reset();
|
||||
|
||||
base.Reset(options);
|
||||
}
|
||||
|
||||
public new void Clear()
|
||||
public void Clear(in AcSerializerOptions? options)
|
||||
{
|
||||
base.Reset();
|
||||
_refTracker.Reset();
|
||||
_propertiesToResolve?.Clear();
|
||||
ChainTracker = null;
|
||||
|
||||
Reset(options);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RegisterObject(int id, object obj)
|
||||
{
|
||||
if (!UseReferenceHandling) return;
|
||||
if (ReferenceHandling == ReferenceHandlingMode.None) return;
|
||||
_refTracker.RegisterObject(id, obj);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ public static partial class AcJsonDeserializer
|
|||
|
||||
try
|
||||
{
|
||||
if (!options.UseReferenceHandling)
|
||||
if (options.ReferenceHandling == ReferenceHandlingMode.None)
|
||||
{
|
||||
var reader = new Utf8JsonReader(utf8Json, new JsonReaderOptions { MaxDepth = options.MaxDepth });
|
||||
if (!reader.Read()) return default;
|
||||
|
|
@ -77,7 +77,7 @@ public static partial class AcJsonDeserializer
|
|||
}
|
||||
finally
|
||||
{
|
||||
JsonDeserializationContextPool.Return(context);
|
||||
JsonDeserializationContextPool.Return(context, options);
|
||||
}
|
||||
}
|
||||
catch (AcJsonDeserializationException) { throw; }
|
||||
|
|
@ -121,7 +121,7 @@ public static partial class AcJsonDeserializer
|
|||
|
||||
ValidateJson(json, targetType);
|
||||
|
||||
if (!options.UseReferenceHandling)
|
||||
if (options.ReferenceHandling == ReferenceHandlingMode.None)
|
||||
{
|
||||
return DeserializeWithUtf8Reader<T>(json, options.MaxDepth);
|
||||
}
|
||||
|
|
@ -136,7 +136,7 @@ public static partial class AcJsonDeserializer
|
|||
}
|
||||
finally
|
||||
{
|
||||
JsonDeserializationContextPool.Return(context);
|
||||
JsonDeserializationContextPool.Return(context, options);
|
||||
}
|
||||
}
|
||||
catch (AcJsonDeserializationException) { throw; }
|
||||
|
|
@ -186,7 +186,7 @@ public static partial class AcJsonDeserializer
|
|||
ValidateJson(json, targetType);
|
||||
|
||||
// Fast path for no reference handling
|
||||
if (!options.UseReferenceHandling)
|
||||
if (options.ReferenceHandling == ReferenceHandlingMode.None)
|
||||
{
|
||||
return DeserializeWithUtf8ReaderNonGeneric(json, targetType, options.MaxDepth);
|
||||
}
|
||||
|
|
@ -203,7 +203,7 @@ public static partial class AcJsonDeserializer
|
|||
}
|
||||
finally
|
||||
{
|
||||
JsonDeserializationContextPool.Return(context);
|
||||
JsonDeserializationContextPool.Return(context, options);
|
||||
}
|
||||
}
|
||||
catch (AcJsonDeserializationException) { throw; }
|
||||
|
|
@ -269,7 +269,7 @@ public static partial class AcJsonDeserializer
|
|||
try
|
||||
{
|
||||
// Fast path for no reference handling
|
||||
if (!options.UseReferenceHandling)
|
||||
if (options.ReferenceHandling == ReferenceHandlingMode.None)
|
||||
{
|
||||
var reader = new Utf8JsonReader(utf8Json, new JsonReaderOptions { MaxDepth = options.MaxDepth });
|
||||
if (!reader.Read()) return null;
|
||||
|
|
@ -288,7 +288,7 @@ public static partial class AcJsonDeserializer
|
|||
}
|
||||
finally
|
||||
{
|
||||
JsonDeserializationContextPool.Return(context);
|
||||
JsonDeserializationContextPool.Return(context, options);
|
||||
}
|
||||
}
|
||||
catch (AcJsonDeserializationException) { throw; }
|
||||
|
|
@ -363,7 +363,7 @@ public static partial class AcJsonDeserializer
|
|||
ValidateJson(json, targetType);
|
||||
|
||||
// Fast path for no reference handling - use Utf8JsonReader streaming
|
||||
if (!options.UseReferenceHandling)
|
||||
if (options.ReferenceHandling == ReferenceHandlingMode.None)
|
||||
{
|
||||
var firstChar = json[0];
|
||||
|
||||
|
|
@ -418,7 +418,7 @@ public static partial class AcJsonDeserializer
|
|||
}
|
||||
finally
|
||||
{
|
||||
JsonDeserializationContextPool.Return(context);
|
||||
JsonDeserializationContextPool.Return(context, options);
|
||||
}
|
||||
}
|
||||
catch (AcJsonDeserializationException) { throw; }
|
||||
|
|
@ -508,7 +508,7 @@ public static partial class AcJsonDeserializer
|
|||
}
|
||||
catch
|
||||
{
|
||||
JsonDeserializationContextPool.Return(context);
|
||||
JsonDeserializationContextPool.Return(context, options);
|
||||
doc.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
|
@ -621,7 +621,7 @@ public static partial class AcJsonDeserializer
|
|||
|
||||
if (_context != null)
|
||||
{
|
||||
JsonDeserializationContextPool.Return(_context);
|
||||
JsonDeserializationContextPool.Return(_context, null);
|
||||
_context = null;
|
||||
}
|
||||
_document?.Dispose();
|
||||
|
|
|
|||
|
|
@ -43,9 +43,6 @@ public static partial class AcJsonSerializer
|
|||
// Use shared reference tracker from AcSerializerCommon
|
||||
private readonly AcSerializerCommon.SerializationReferenceTracker _refTracker = new();
|
||||
|
||||
public bool UseReferenceHandling { get; private set; }
|
||||
public byte MaxDepth { get; private set; }
|
||||
|
||||
private static readonly JsonWriterOptions WriterOptions = new()
|
||||
{
|
||||
Indented = false,
|
||||
|
|
@ -65,16 +62,16 @@ public static partial class AcJsonSerializer
|
|||
protected override Func<Type, JsonSerializeTypeMetadata> MetadataFactory
|
||||
=> static t => new JsonSerializeTypeMetadata(t);
|
||||
|
||||
public void Reset(in AcJsonSerializerOptions options)
|
||||
public override void Reset(in AcSerializerOptions options)
|
||||
{
|
||||
UseReferenceHandling = options.UseReferenceHandling;
|
||||
MaxDepth = options.MaxDepth;
|
||||
_refTracker.Reset();
|
||||
|
||||
if (UseReferenceHandling)
|
||||
if (ReferenceHandling != ReferenceHandlingMode.None)
|
||||
{
|
||||
_refTracker.EnsureInitialized();
|
||||
}
|
||||
|
||||
base.Reset(options);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ public static partial class AcJsonSerializer
|
|||
var context = SerializationContextPool.Get(options);
|
||||
try
|
||||
{
|
||||
if (options.UseReferenceHandling)
|
||||
if (options.ReferenceHandling != ReferenceHandlingMode.None)
|
||||
ScanReferences(actualValue, context, 0);
|
||||
|
||||
WriteValue(actualValue, context, 0);
|
||||
|
|
@ -182,7 +182,7 @@ public static partial class AcJsonSerializer
|
|||
var metadata = wrapper.Metadata;
|
||||
|
||||
// Use IId-aware reference handling
|
||||
if (context.UseReferenceHandling && context.TryGetExistingRefForIId(value, metadata, out var refId))
|
||||
if (context.ReferenceHandling != ReferenceHandlingMode.None && context.TryGetExistingRefForIId(value, metadata, out var refId))
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString(RefPropertyEncoded, refId.ToString(CultureInfo.InvariantCulture));
|
||||
|
|
@ -192,7 +192,7 @@ public static partial class AcJsonSerializer
|
|||
|
||||
writer.WriteStartObject();
|
||||
|
||||
if (context.UseReferenceHandling && context.ShouldWriteIdForIId(value, metadata, out var id))
|
||||
if (context.ReferenceHandling != ReferenceHandlingMode.None && context.ShouldWriteIdForIId(value, metadata, out var id))
|
||||
{
|
||||
writer.WriteString(IdPropertyEncoded, id.ToString(CultureInfo.InvariantCulture));
|
||||
context.MarkAsWrittenForIId(value, metadata, id);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,27 @@ public enum AcSerializerType : byte
|
|||
Toon = 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference handling mode for serialization.
|
||||
/// </summary>
|
||||
public enum ReferenceHandlingMode : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// No reference handling - all objects serialized inline.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Reference handling only for IId objects - uses semantic Id for deduplication.
|
||||
/// </summary>
|
||||
OnlyId = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Full reference handling for all objects (future use).
|
||||
/// </summary>
|
||||
All = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for custom property mapping during cross-type deserialization/population.
|
||||
/// Enables mapping between different class hierarchies or renamed properties.
|
||||
|
|
@ -23,10 +44,10 @@ public abstract class AcSerializerOptions
|
|||
public abstract AcSerializerType SerializerType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use $id/$ref reference handling for circular references.
|
||||
/// Default: true
|
||||
/// Reference handling mode for circular/shared references.
|
||||
/// Default: OnlyId (handles IId objects)
|
||||
/// </summary>
|
||||
public bool UseReferenceHandling { get; init; } = true;
|
||||
public ReferenceHandlingMode ReferenceHandling { get; init; } = ReferenceHandlingMode.OnlyId;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum depth for serialization/deserialization.
|
||||
|
|
@ -69,7 +90,7 @@ public sealed class AcJsonSerializerOptions : AcSerializerOptions
|
|||
/// <summary>
|
||||
/// Options for shallow serialization (root level only, no references).
|
||||
/// </summary>
|
||||
public static readonly AcJsonSerializerOptions ShallowCopy = new() { MaxDepth = 0, UseReferenceHandling = false };
|
||||
public static readonly AcJsonSerializerOptions ShallowCopy = new() { MaxDepth = 0, ReferenceHandling = ReferenceHandlingMode.None };
|
||||
|
||||
/// <summary>
|
||||
/// Creates options with specified max depth.
|
||||
|
|
@ -79,5 +100,5 @@ public sealed class AcJsonSerializerOptions : AcSerializerOptions
|
|||
/// <summary>
|
||||
/// Creates options without reference handling.
|
||||
/// </summary>
|
||||
public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false };
|
||||
public static AcJsonSerializerOptions WithoutReferenceHandling() => new() { ReferenceHandling = ReferenceHandlingMode.None };
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using AyCode.Core.Serializers.Jsons;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
|
@ -102,9 +103,9 @@ public abstract class SerializationContextBase<TMetadata> : AcSerializerContextB
|
|||
/// <summary>
|
||||
/// Resets serialization-specific state. Called by derived classes.
|
||||
/// </summary>
|
||||
public override void Reset()
|
||||
public override void Reset(in AcSerializerOptions options)
|
||||
{
|
||||
base.Reset();
|
||||
base.Reset(options);
|
||||
// Future: Reset serialization-specific state
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Collections;
|
|||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
using static AyCode.Core.Helpers.JsonUtilities;
|
||||
|
||||
namespace AyCode.Core.Serializers.Toons;
|
||||
|
|
@ -53,7 +54,7 @@ public static partial class AcToonSerializer
|
|||
}
|
||||
|
||||
// Check for reference
|
||||
if (context.UseReferenceHandling && context.TryGetExistingRef(value, out var refId))
|
||||
if (context.ReferenceHandling != ReferenceHandlingMode.None && context.TryGetExistingRef(value, out var refId))
|
||||
{
|
||||
context.Write($"@ref:{refId}");
|
||||
return;
|
||||
|
|
@ -227,7 +228,7 @@ public static partial class AcToonSerializer
|
|||
var metadata = GetTypeMetadata(type);
|
||||
|
||||
// Write reference ID if this is a multi-referenced object
|
||||
if (context.UseReferenceHandling && context.ShouldWriteRef(value, out var refId))
|
||||
if (context.ReferenceHandling != ReferenceHandlingMode.None && context.ShouldWriteRef(value, out var refId))
|
||||
{
|
||||
context.Write($"@{refId} ");
|
||||
context.MarkAsWritten(value, refId);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ using System.Collections.Generic;
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
|
||||
//using ReferenceEqualityComparer = AyCode.Core.Serializers.ReferenceEqualityComparer;
|
||||
|
||||
namespace AyCode.Core.Serializers.Toons;
|
||||
|
|
@ -58,8 +60,6 @@ public static partial class AcToonSerializer
|
|||
|
||||
public AcToonSerializerOptions Options { get; private set; }
|
||||
public int CurrentIndentLevel { get; set; }
|
||||
public bool UseReferenceHandling { get; private set; }
|
||||
public byte MaxDepth { get; private set; }
|
||||
|
||||
public ToonSerializationContext(AcToonSerializerOptions options)
|
||||
{
|
||||
|
|
@ -77,12 +77,10 @@ public static partial class AcToonSerializer
|
|||
public void Reset(AcToonSerializerOptions options)
|
||||
{
|
||||
Options = options;
|
||||
UseReferenceHandling = options.UseReferenceHandling;
|
||||
MaxDepth = options.MaxDepth;
|
||||
CurrentIndentLevel = 0;
|
||||
_nextRefId = 1;
|
||||
|
||||
if (UseReferenceHandling)
|
||||
if (ReferenceHandling != ReferenceHandlingMode.None)
|
||||
{
|
||||
_scanOccurrences ??= new Dictionary<object, int>(64, ReferenceEqualityComparer.Instance);
|
||||
_writtenRefs ??= new Dictionary<object, int>(32, ReferenceEqualityComparer.Instance);
|
||||
|
|
@ -90,6 +88,7 @@ public static partial class AcToonSerializer
|
|||
}
|
||||
|
||||
_registeredTypes ??= new HashSet<Type>(16);
|
||||
base.Reset(options);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Collections.Concurrent;
|
|||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
using static AyCode.Core.Helpers.JsonUtilities;
|
||||
|
||||
namespace AyCode.Core.Serializers.Toons;
|
||||
|
|
@ -50,7 +51,7 @@ public static partial class AcToonSerializer
|
|||
try
|
||||
{
|
||||
// Reference scanning if needed
|
||||
if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(type))
|
||||
if (context.ReferenceHandling != ReferenceHandlingMode.None && !IsPrimitiveOrStringFast(type))
|
||||
{
|
||||
ScanReferences(value, context, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions
|
|||
UseIndentation = true,
|
||||
OmitDefaultValues = true,
|
||||
WriteTypeNames = false,
|
||||
UseReferenceHandling = false
|
||||
ReferenceHandling = ReferenceHandlingMode.None
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -210,5 +210,5 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions
|
|||
/// <summary>
|
||||
/// Creates options without reference handling (faster, no circular reference support).
|
||||
/// </summary>
|
||||
public static AcToonSerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false };
|
||||
public static AcToonSerializerOptions WithoutReferenceHandling() => new() { ReferenceHandling = ReferenceHandlingMode.None };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1071,6 +1071,6 @@ public class SignalRClientToHubTest_Binary_NoRef : SignalRClientToHubTestBase
|
|||
{
|
||||
protected override AcSerializerOptions SerializerOption { get; } = new AcBinarySerializerOptions
|
||||
{
|
||||
UseReferenceHandling = false
|
||||
ReferenceHandling = ReferenceHandlingMode.None
|
||||
};
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ namespace AyCode.Services.Server.Tests.SignalRs.SignalRDatasources;
|
|||
[TestClass]
|
||||
public class SignalRDataSourceTests_List_Binary_NoRef : SignalRDataSourceTestBase<TestOrderItemListDataSource, List<TestOrderItem>>
|
||||
{
|
||||
protected override AcSerializerOptions SerializerOption => new AcBinarySerializerOptions { UseReferenceHandling = false };
|
||||
protected override AcSerializerOptions SerializerOption => new AcBinarySerializerOptions { ReferenceHandling = ReferenceHandlingMode.None };
|
||||
protected override TestOrderItemListDataSource CreateDataSource(TestableSignalRClient2 client, SignalRCrudTags crudTags)
|
||||
=> new(client, crudTags);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue