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,
|
UseMetadata = false,
|
||||||
UseStringInterning = false,
|
UseStringInterning = false,
|
||||||
UseReferenceHandling = false
|
ReferenceHandling = ReferenceHandlingMode.None,
|
||||||
},
|
},
|
||||||
_ => new AcBinarySerializerOptions()
|
_ => new AcBinarySerializerOptions()
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MessagePack" Version="3.1.4" />
|
<PackageReference Include="MessagePack" Version="3.1.4" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,80 +1,128 @@
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using AyCode.Core.Serializers.Binaries;
|
using AyCode.Core.Serializers.Binaries;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
using MessagePack.Resolvers;
|
using MessagePack.Resolvers;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Console;
|
namespace AyCode.Core.Serializers.Console;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Console application for Performance Diagnostics profiling.
|
/// Comprehensive benchmark application for all serializers.
|
||||||
/// Run with: Debug > Performance Profiler in Visual Studio
|
/// Compares: AcBinary (all options), AcJson, MessagePack, Newtonsoft.Json, System.Text.Json
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// Usage:
|
||||||
/// dotnet run -- serialize # Profile serialize only
|
/// dotnet run # Run all benchmarks
|
||||||
/// dotnet run -- deserialize # Profile deserialize only
|
/// dotnet run -- quick # Quick mode (fewer iterations)
|
||||||
/// dotnet run -- all # Profile both (default)
|
/// dotnet run -- serialize # Serialize only
|
||||||
|
/// dotnet run -- deserialize # Deserialize only
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Program
|
public static class Program
|
||||||
{
|
{
|
||||||
private const int WarmupIterations = 50;
|
private const string ResultsDirectory = @"H:\Applications\Aycode\Source\AyCode.Core\Test_Benchmark_Results\Benchmark";
|
||||||
private const int TestIterations = 5000;
|
|
||||||
|
#if DEBUG
|
||||||
// Keep references to prevent GC during profiling
|
private const string BuildConfiguration = "Debug";
|
||||||
private static TestOrder s_testOrder = null!;
|
#else
|
||||||
private static byte[] s_acBinaryData = null!;
|
private const string BuildConfiguration = "Release";
|
||||||
private static byte[] s_acBinaryNoRefData = null!;
|
#endif
|
||||||
private static byte[] s_msgPackData = null!;
|
|
||||||
private static AcBinarySerializerOptions s_acBinaryOptions = null!;
|
// Serializer name constants
|
||||||
private static AcBinarySerializerOptions s_acBinaryNoRefOptions = null!;
|
private const string SerializerMessagePack = "MessagePack";
|
||||||
private static MessagePackSerializerOptions s_msgPackOptions = null!;
|
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)
|
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";
|
var mode = args.Length > 0 ? args[0].ToLower() : "all";
|
||||||
|
|
||||||
System.Console.WriteLine("=".PadRight(60, '='));
|
if (mode == "quick")
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
case "serialize":
|
WarmupIterations = 10;
|
||||||
case "ser":
|
TestIterations = 100;
|
||||||
RunSerializeTests();
|
mode = "all";
|
||||||
break;
|
|
||||||
case "deserialize":
|
|
||||||
case "des":
|
|
||||||
RunDeserializeTests();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
RunSerializeTests();
|
|
||||||
RunDeserializeTests();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sw.Stop();
|
System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════╗");
|
||||||
System.Console.WriteLine($"\nTotal time: {sw.ElapsedMilliseconds:N0} ms");
|
System.Console.WriteLine("║ COMPREHENSIVE SERIALIZER BENCHMARK SUITE ║");
|
||||||
System.Console.WriteLine("=".PadRight(60, '='));
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print grouped results
|
||||||
|
PrintGroupedResults(allResults, testDataSets);
|
||||||
|
|
||||||
|
// Save results to file
|
||||||
|
SaveResults(allResults, testDataSets);
|
||||||
|
|
||||||
|
System.Console.WriteLine("\n✓ Benchmark complete!");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Setup()
|
#region Test Data Creation
|
||||||
|
|
||||||
|
private static List<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();
|
TestDataFactory.ResetIdCounter();
|
||||||
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
||||||
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
||||||
var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);
|
var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);
|
||||||
|
|
||||||
s_testOrder = TestDataFactory.CreateOrder(
|
var order = TestDataFactory.CreateOrder(
|
||||||
itemCount: 3,
|
itemCount: 3,
|
||||||
palletsPerItem: 3,
|
palletsPerItem: 3,
|
||||||
measurementsPerPallet: 3,
|
measurementsPerPallet: 3,
|
||||||
|
|
@ -83,113 +131,632 @@ public static class Program
|
||||||
sharedUser: sharedUser,
|
sharedUser: sharedUser,
|
||||||
sharedMetadata: sharedMeta);
|
sharedMetadata: sharedMeta);
|
||||||
|
|
||||||
s_acBinaryOptions = AcBinarySerializerOptions.Default;
|
return new TestDataSet("Medium (3x3x3x4, shared refs)", order);
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)...");
|
System.Console.WriteLine($"Warming up ({WarmupIterations} iterations)...");
|
||||||
for (int i = 0; i < WarmupIterations; i++)
|
foreach (var serializer in serializers)
|
||||||
{
|
{
|
||||||
DoSerializeAcBinary();
|
serializer.Warmup(WarmupIterations);
|
||||||
DoSerializeAcBinaryNoRef();
|
|
||||||
DoSerializeMsgPack();
|
|
||||||
DoDeserializeAcBinary();
|
|
||||||
DoDeserializeAcBinaryNoRef();
|
|
||||||
DoDeserializeMsgPack();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run benchmarks
|
||||||
|
System.Console.WriteLine($"Running benchmarks ({TestIterations} iterations)...\n");
|
||||||
|
|
||||||
|
foreach (var serializer in serializers)
|
||||||
|
{
|
||||||
|
var result = new BenchmarkResult
|
||||||
|
{
|
||||||
|
TestDataName = testData.Name,
|
||||||
|
SerializerName = serializer.Name,
|
||||||
|
SerializedSize = serializer.SerializedSize
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mode is "all" or "serialize" or "ser")
|
||||||
|
{
|
||||||
|
result.SerializeTimeMs = RunTimed(() => serializer.Serialize(), TestIterations);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode is "all" or "deserialize" or "des")
|
||||||
|
{
|
||||||
|
result.DeserializeTimeMs = RunTimed(() => serializer.Deserialize(), TestIterations);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.Add(result);
|
||||||
|
PrintResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RunSerializeTests()
|
private static List<ISerializerBenchmark> CreateSerializers(TestDataSet testData)
|
||||||
{
|
{
|
||||||
System.Console.WriteLine("--- SERIALIZE ---");
|
return new List<ISerializerBenchmark>
|
||||||
|
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
for (int i = 0; i < TestIterations; i++)
|
|
||||||
{
|
{
|
||||||
DoSerializeAcBinary();
|
// AcBinary variants
|
||||||
}
|
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
|
||||||
sw.Stop();
|
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling(), SerializerAcBinaryNoRef),
|
||||||
System.Console.WriteLine($" AcBinary (WithRef): {sw.ElapsedMilliseconds,6:N0} ms");
|
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
|
||||||
|
new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = false }, SerializerAcBinaryNoIntern),
|
||||||
sw.Restart();
|
|
||||||
for (int i = 0; i < TestIterations; i++)
|
// AcJson
|
||||||
{
|
new AcJsonBenchmark(testData.Order, AcJsonSerializerOptions.Default, SerializerAcJsonDefault),
|
||||||
DoSerializeAcBinaryNoRef();
|
|
||||||
}
|
// MessagePack
|
||||||
sw.Stop();
|
new MessagePackBenchmark(testData.Order, SerializerMessagePack),
|
||||||
System.Console.WriteLine($" AcBinary (NoRef): {sw.ElapsedMilliseconds,6:N0} ms");
|
|
||||||
|
// Newtonsoft.Json
|
||||||
sw.Restart();
|
new NewtonsoftBenchmark(testData.Order, SerializerNewtonsoftJson),
|
||||||
for (int i = 0; i < TestIterations; i++)
|
|
||||||
{
|
// System.Text.Json
|
||||||
DoSerializeMsgPack();
|
new SystemTextJsonBenchmark(testData.Order, SerializerSystemTextJson)
|
||||||
}
|
};
|
||||||
sw.Stop();
|
|
||||||
System.Console.WriteLine($" MessagePack: {sw.ElapsedMilliseconds,6:N0} ms");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RunDeserializeTests()
|
private static double RunTimed(Action action, int iterations)
|
||||||
{
|
{
|
||||||
System.Console.WriteLine("--- DESERIALIZE ---");
|
|
||||||
|
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
for (int i = 0; i < TestIterations; i++)
|
for (var i = 0; i < iterations; i++)
|
||||||
{
|
{
|
||||||
DoDeserializeAcBinary();
|
action();
|
||||||
}
|
}
|
||||||
sw.Stop();
|
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++)
|
|
||||||
{
|
|
||||||
DoDeserializeAcBinaryNoRef();
|
|
||||||
}
|
|
||||||
sw.Stop();
|
|
||||||
System.Console.WriteLine($" AcBinary (NoRef): {sw.ElapsedMilliseconds,6:N0} ms");
|
|
||||||
|
|
||||||
sw.Restart();
|
|
||||||
for (int i = 0; i < TestIterations; i++)
|
|
||||||
{
|
|
||||||
DoDeserializeMsgPack();
|
|
||||||
}
|
|
||||||
sw.Stop();
|
|
||||||
System.Console.WriteLine($" MessagePack: {sw.ElapsedMilliseconds,6:N0} ms");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separate methods for better profiler visibility - NO INLINING
|
#endregion
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
||||||
private static byte[] DoSerializeAcBinary()
|
|
||||||
=> AcBinarySerializer.Serialize(s_testOrder, s_acBinaryOptions);
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
#region Serializer Implementations
|
||||||
private static byte[] DoSerializeAcBinaryNoRef()
|
|
||||||
=> AcBinarySerializer.Serialize(s_testOrder, s_acBinaryNoRefOptions);
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
private interface ISerializerBenchmark
|
||||||
private static byte[] DoSerializeMsgPack()
|
{
|
||||||
=> MessagePackSerializer.Serialize(s_testOrder, s_msgPackOptions);
|
string Name { get; }
|
||||||
|
int SerializedSize { get; }
|
||||||
|
void Warmup(int iterations);
|
||||||
|
void Serialize();
|
||||||
|
void Deserialize();
|
||||||
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
private sealed class AcBinaryBenchmark : ISerializerBenchmark
|
||||||
private static TestOrder? DoDeserializeAcBinary()
|
{
|
||||||
=> AcBinaryDeserializer.Deserialize<TestOrder>(s_acBinaryData);
|
private readonly TestOrder _order;
|
||||||
|
private readonly AcBinarySerializerOptions _options;
|
||||||
|
private readonly byte[] _serialized;
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
public string Name { get; }
|
||||||
private static TestOrder? DoDeserializeAcBinaryNoRef()
|
public int SerializedSize => _serialized.Length;
|
||||||
=> AcBinaryDeserializer.Deserialize<TestOrder>(s_acBinaryNoRefData);
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
public AcBinaryBenchmark(TestOrder order, AcBinarySerializerOptions options, string name)
|
||||||
private static TestOrder? DoDeserializeMsgPack()
|
{
|
||||||
=> MessagePackSerializer.Deserialize<TestOrder>(s_msgPackData, s_msgPackOptions);
|
_order = order;
|
||||||
|
_options = options;
|
||||||
|
Name = name;
|
||||||
|
_serialized = AcBinarySerializer.Serialize(order, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Warmup(int iterations)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < iterations; i++)
|
||||||
|
{
|
||||||
|
Serialize();
|
||||||
|
Deserialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
public void Serialize() => AcBinarySerializer.Serialize(_order, _options);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
public void Deserialize() => AcBinaryDeserializer.Deserialize<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)]
|
||||||
|
public void Serialize() => AcJsonSerializer.Serialize(_order, _options);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
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)]
|
||||||
|
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);
|
Assert.AreEqual("SharedProduct", result.Items[1].Product.Name);
|
||||||
|
|
||||||
// With reference handling, they should be the same instance
|
// 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)}");
|
Console.WriteLine($"Same instance: {ReferenceEquals(result.Items[0].Product, result.Items[1].Product)}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -781,7 +781,7 @@ public static class AcSerializerCommon
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Common reference tracking for serialization.
|
/// 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)
|
/// - IId objects: hash = Id (positive, DB guarantees uniqueness per entity type)
|
||||||
/// - Non-IId objects: hash = RuntimeHelpers.GetHashCode | 0x80000000 (negative)
|
/// - Non-IId objects: hash = RuntimeHelpers.GetHashCode | 0x80000000 (negative)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -789,9 +789,6 @@ public static class AcSerializerCommon
|
||||||
{
|
{
|
||||||
private const int InitialCapacity = 128;
|
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)
|
// Unified HashSet for seen hashes (both IId and Reference)
|
||||||
private HashSet<int>? _seenHashes;
|
private HashSet<int>? _seenHashes;
|
||||||
|
|
||||||
|
|
@ -809,7 +806,6 @@ public static class AcSerializerCommon
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
_nextRefId = 1;
|
_nextRefId = 1;
|
||||||
_bloom0 = _bloom1 = _bloom2 = _bloom3 = 0;
|
|
||||||
_seenHashes?.Clear();
|
_seenHashes?.Clear();
|
||||||
_multiRefHashes?.Clear();
|
_multiRefHashes?.Clear();
|
||||||
_writtenRefs?.Clear();
|
_writtenRefs?.Clear();
|
||||||
|
|
@ -878,41 +874,14 @@ public static class AcSerializerCommon
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Core tracking logic using Bloom filter + HashSet.
|
/// Core tracking logic using HashSet.Add() which returns false if already exists.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private bool TrackHash(int hash)
|
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);
|
_seenHashes ??= new HashSet<int>(InitialCapacity);
|
||||||
|
|
||||||
|
// HashSet.Add returns false if element already exists
|
||||||
if (!_seenHashes.Add(hash))
|
if (!_seenHashes.Add(hash))
|
||||||
{
|
{
|
||||||
// Already seen - multi-referenced!
|
// Already seen - multi-referenced!
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
@ -14,6 +15,8 @@ namespace AyCode.Core.Serializers;
|
||||||
/// <typeparam name="TMetadata">The concrete metadata type.</typeparam>
|
/// <typeparam name="TMetadata">The concrete metadata type.</typeparam>
|
||||||
public abstract class AcSerializerContextBase<TMetadata> where TMetadata : TypeMetadataBase
|
public abstract class AcSerializerContextBase<TMetadata> where TMetadata : TypeMetadataBase
|
||||||
{
|
{
|
||||||
|
public byte MaxDepth { get; private set; }
|
||||||
|
public ReferenceHandlingMode ReferenceHandling { get; internal set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Global shared cache for metadata (thread-safe, shared across all contexts).
|
/// Global shared cache for metadata (thread-safe, shared across all contexts).
|
||||||
/// Generic specialization ensures separate cache per TMetadata type.
|
/// 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.
|
/// Resets all wrapper tracking states for reuse.
|
||||||
/// Does not remove wrappers - keeps them for next operation.
|
/// Does not remove wrappers - keeps them for next operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void Reset()
|
public virtual void Reset(in AcSerializerOptions? options)
|
||||||
{
|
{
|
||||||
foreach (var wrapper in _wrappers.Values)
|
foreach (var wrapper in _wrappers.Values)
|
||||||
{
|
{
|
||||||
wrapper.ResetTracking();
|
wrapper.ResetTracking();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options == null) return;
|
||||||
|
|
||||||
|
MaxDepth = options.MaxDepth;
|
||||||
|
ReferenceHandling = options.ReferenceHandling;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using AyCode.Core.Serializers.Jsons;
|
||||||
|
using System;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
@ -32,7 +33,10 @@ public static partial class AcBinaryDeserializer
|
||||||
public readonly BinaryDeserializationContextClass ContextClass;
|
public readonly BinaryDeserializationContextClass ContextClass;
|
||||||
|
|
||||||
public bool HasMetadata { get; private set; }
|
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 IsMergeMode { readonly get; set; }
|
||||||
public bool RemoveOrphanedItems { readonly get; set; }
|
public bool RemoveOrphanedItems { readonly get; set; }
|
||||||
public bool IsAtEnd => _position >= _buffer.Length;
|
public bool IsAtEnd => _position >= _buffer.Length;
|
||||||
|
|
@ -69,7 +73,6 @@ public static partial class AcBinaryDeserializer
|
||||||
//_objectReferences = null;
|
//_objectReferences = null;
|
||||||
_stringCache = null;
|
_stringCache = null;
|
||||||
HasMetadata = false;
|
HasMetadata = false;
|
||||||
HasReferenceHandling = false;
|
|
||||||
IsMergeMode = false;
|
IsMergeMode = false;
|
||||||
RemoveOrphanedItems = false;
|
RemoveOrphanedItems = false;
|
||||||
ChainTracker = null;
|
ChainTracker = null;
|
||||||
|
|
@ -77,6 +80,8 @@ public static partial class AcBinaryDeserializer
|
||||||
_useStringCaching = options.UseStringCaching;
|
_useStringCaching = options.UseStringCaching;
|
||||||
_maxCachedStringLength = options.MaxCachedStringLength;
|
_maxCachedStringLength = options.MaxCachedStringLength;
|
||||||
ContextClass = contextClass;
|
ContextClass = contextClass;
|
||||||
|
// Initialize ReferenceHandling from options (will be overwritten by ReadHeader if present in stream)
|
||||||
|
ContextClass.ReferenceHandling = options.ReferenceHandling;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ReadHeader()
|
public void ReadHeader()
|
||||||
|
|
@ -97,22 +102,38 @@ public static partial class AcBinaryDeserializer
|
||||||
var marker = ReadByteInternal();
|
var marker = ReadByteInternal();
|
||||||
var hasPropertyTable = false;
|
var hasPropertyTable = false;
|
||||||
var hasInternTable = false;
|
var hasInternTable = false;
|
||||||
|
var hasInternFooter = false;
|
||||||
|
var footerPosition = 0;
|
||||||
|
|
||||||
if (marker == BinaryTypeCode.MetadataHeader)
|
if (marker == BinaryTypeCode.MetadataHeader)
|
||||||
{
|
{
|
||||||
hasPropertyTable = true;
|
hasPropertyTable = true;
|
||||||
HasReferenceHandling = true;
|
ContextClass.ReferenceHandling = ReferenceHandlingMode.OnlyId; // Legacy: assume OnlyId
|
||||||
}
|
}
|
||||||
else if (marker == BinaryTypeCode.NoMetadataHeader)
|
else if (marker == BinaryTypeCode.NoMetadataHeader)
|
||||||
{
|
{
|
||||||
HasReferenceHandling = true;
|
ContextClass.ReferenceHandling = ReferenceHandlingMode.OnlyId; // Legacy: assume OnlyId
|
||||||
}
|
}
|
||||||
else if ((marker & 0xF0) == BinaryTypeCode.HeaderFlagsBase)
|
else if ((marker & 0xF0) == BinaryTypeCode.HeaderFlagsBase)
|
||||||
{
|
{
|
||||||
var flags = (byte)(marker & 0x0F);
|
var flags = (byte)(marker & 0x0F);
|
||||||
hasPropertyTable = (flags & BinaryTypeCode.HeaderFlag_Metadata) != 0;
|
hasPropertyTable = (flags & BinaryTypeCode.HeaderFlag_Metadata) != 0;
|
||||||
HasReferenceHandling = (flags & BinaryTypeCode.HeaderFlag_ReferenceHandling) != 0;
|
// Decode ReferenceHandlingMode from separate bits
|
||||||
hasInternTable = (flags & BinaryTypeCode.HeaderFlag_StringInternTable) != 0;
|
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
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -133,6 +154,7 @@ public static partial class AcBinaryDeserializer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy: interned strings in header
|
||||||
if (hasInternTable)
|
if (hasInternTable)
|
||||||
{
|
{
|
||||||
var internCount = (int)ReadVarUInt();
|
var internCount = (int)ReadVarUInt();
|
||||||
|
|
@ -142,6 +164,36 @@ public static partial class AcBinaryDeserializer
|
||||||
_internedStrings.Add(ReadHeaderString());
|
_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)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
|
@ -55,10 +56,6 @@ public static partial class AcBinarySerializer
|
||||||
private const int PropertyStateBufferMaxCache = 512;
|
private const int PropertyStateBufferMaxCache = 512;
|
||||||
private const int InitialInternCapacity = 32;
|
private const int InitialInternCapacity = 32;
|
||||||
private const int InitialPropertyNameCapacity = 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 byte[] _buffer;
|
||||||
private int _position;
|
private int _position;
|
||||||
|
|
@ -69,25 +66,14 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
private Dictionary<string, int>? _internedStrings;
|
private Dictionary<string, int>? _internedStrings;
|
||||||
private List<string>? _internedStringList;
|
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 Dictionary<string, int>? _propertyNames;
|
||||||
private List<string>? _propertyNameList;
|
private List<string>? _propertyNameList;
|
||||||
private int[]? _propertyIndexBuffer;
|
private int[]? _propertyIndexBuffer;
|
||||||
private byte[]? _propertyStateBuffer;
|
private byte[]? _propertyStateBuffer;
|
||||||
|
|
||||||
public bool UseReferenceHandling { get; private set; }
|
|
||||||
public bool UseStringInterning { get; private set; }
|
public bool UseStringInterning { get; private set; }
|
||||||
public bool UseMetadata { get; private set; }
|
public bool UseMetadata { get; private set; }
|
||||||
public byte MaxDepth { get; private set; }
|
|
||||||
public byte MinStringInternLength { get; private set; }
|
public byte MinStringInternLength { get; private set; }
|
||||||
public byte MaxStringInternLength { get; private set; }
|
public byte MaxStringInternLength { get; private set; }
|
||||||
public BinaryPropertyFilter? PropertyFilter { get; private set; }
|
public BinaryPropertyFilter? PropertyFilter { get; private set; }
|
||||||
|
|
@ -109,25 +95,17 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
public void Reset(AcBinarySerializerOptions options)
|
public void Reset(AcBinarySerializerOptions options)
|
||||||
{
|
{
|
||||||
|
// Reset wrapper tracking state from base class (IId tracking)
|
||||||
|
base.Reset(options);
|
||||||
|
|
||||||
_position = 0;
|
_position = 0;
|
||||||
UseReferenceHandling = options.UseReferenceHandling;
|
|
||||||
UseStringInterning = options.UseStringInterning;
|
UseStringInterning = options.UseStringInterning;
|
||||||
UseMetadata = options.UseMetadata;
|
UseMetadata = options.UseMetadata;
|
||||||
MaxDepth = options.MaxDepth;
|
|
||||||
MinStringInternLength = options.MinStringInternLength;
|
MinStringInternLength = options.MinStringInternLength;
|
||||||
MaxStringInternLength = options.MaxStringInternLength;
|
MaxStringInternLength = options.MaxStringInternLength;
|
||||||
PropertyFilter = options.PropertyFilter;
|
PropertyFilter = options.PropertyFilter;
|
||||||
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
|
_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)
|
if (_buffer.Length < _initialBufferSize)
|
||||||
{
|
{
|
||||||
ArrayPool<byte>.Shared.Return(_buffer);
|
ArrayPool<byte>.Shared.Return(_buffer);
|
||||||
|
|
@ -138,12 +116,6 @@ public static partial class AcBinarySerializer
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
_position = 0;
|
_position = 0;
|
||||||
|
|
||||||
// Reset bloom filter
|
|
||||||
_bloomFilter0 = 0;
|
|
||||||
_bloomFilter1 = 0;
|
|
||||||
_bloomFilter2 = 0;
|
|
||||||
_bloomFilter3 = 0;
|
|
||||||
|
|
||||||
//_refTracker.Reset();
|
//_refTracker.Reset();
|
||||||
ClearAndTrimIfNeeded(_internedStrings, InitialInternCapacity * 4);
|
ClearAndTrimIfNeeded(_internedStrings, InitialInternCapacity * 4);
|
||||||
|
|
@ -169,7 +141,7 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResetCachedPropertyIndices()
|
private static void ResetCachedPropertyIndices()
|
||||||
{
|
{
|
||||||
// Note: BinaryPropertyAccessor.CachedPropertyNameIndex is per-context,
|
// Note: BinaryPropertyAccessor.CachedPropertyNameIndex is per-context,
|
||||||
// but metadata is cached globally. We reset it during Clear to avoid
|
// but metadata is cached globally. We reset it during Clear to avoid
|
||||||
|
|
@ -205,6 +177,10 @@ public static partial class AcBinarySerializer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private List<byte[]>? _internedStringUtf8;
|
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)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public int RegisterInternedString(string value)
|
public int RegisterInternedString(string value)
|
||||||
{
|
{
|
||||||
|
|
@ -212,32 +188,17 @@ public static partial class AcBinarySerializer
|
||||||
_internedStringList ??= new List<string>(InitialInternCapacity);
|
_internedStringList ??= new List<string>(InitialInternCapacity);
|
||||||
_internedStringUtf8 ??= new List<byte[]>(InitialInternCapacity);
|
_internedStringUtf8 ??= new List<byte[]>(InitialInternCapacity);
|
||||||
|
|
||||||
// Fast path: check bloom filter first
|
// Single operation: lookup + conditional add
|
||||||
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
|
|
||||||
ref var index = ref CollectionsMarshal.GetValueRefOrAddDefault(_internedStrings, value, out var exists);
|
ref var index = ref CollectionsMarshal.GetValueRefOrAddDefault(_internedStrings, value, out var exists);
|
||||||
if (exists)
|
if (exists)
|
||||||
{
|
{
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New string - add to lists
|
||||||
index = _internedStringList.Count;
|
index = _internedStringList.Count;
|
||||||
_internedStringList.Add(value);
|
_internedStringList.Add(value);
|
||||||
// Cache UTF8 bytes immediately
|
|
||||||
_internedStringUtf8.Add(GetUtf8BytesCached(value));
|
_internedStringUtf8.Add(GetUtf8BytesCached(value));
|
||||||
BloomFilterAdd(hash);
|
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,77 +209,16 @@ public static partial class AcBinarySerializer
|
||||||
private static byte[] GetUtf8BytesCached(string value)
|
private static byte[] GetUtf8BytesCached(string value)
|
||||||
{
|
{
|
||||||
// Fast path for ASCII strings - direct char to byte conversion
|
// 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];
|
var bytes = new byte[value.Length];
|
||||||
System.Text.Ascii.FromUtf16(value.AsSpan(), bytes, out _);
|
Ascii.FromUtf16(value.AsSpan(), bytes, out _);
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
// Standard path for multi-byte UTF8
|
// Standard path for multi-byte UTF8
|
||||||
return Utf8NoBom.GetBytes(value);
|
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
|
#endregion
|
||||||
|
|
||||||
#region Property Name Table
|
#region Property Name Table
|
||||||
|
|
@ -746,12 +646,12 @@ public static partial class AcBinarySerializer
|
||||||
public void WriteStringUtf8(string value)
|
public void WriteStringUtf8(string value)
|
||||||
{
|
{
|
||||||
// Fast path for ASCII-only strings using SIMD-optimized check
|
// Fast path for ASCII-only strings using SIMD-optimized check
|
||||||
if (System.Text.Ascii.IsValid(value))
|
if (Ascii.IsValid(value))
|
||||||
{
|
{
|
||||||
WriteVarUInt((uint)value.Length);
|
WriteVarUInt((uint)value.Length);
|
||||||
EnsureCapacity(value.Length);
|
EnsureCapacity(value.Length);
|
||||||
// Use System.Text.Ascii for SIMD-optimized ASCII to bytes conversion
|
// 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;
|
_position += value.Length;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -971,32 +871,28 @@ public static partial class AcBinarySerializer
|
||||||
#region Header and Metadata
|
#region Header and Metadata
|
||||||
|
|
||||||
private int _headerPosition;
|
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>
|
/// <summary>
|
||||||
/// Estimates header payload size based on registered property names and intern strings.
|
/// Estimates header payload size based on registered property names.
|
||||||
/// Call after metadata registration but before writing the body.
|
/// String interning now uses footer, so no estimation needed for strings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int EstimateHeaderPayloadSize()
|
public int EstimateHeaderPayloadSize()
|
||||||
{
|
{
|
||||||
var size = 0;
|
var size = 0;
|
||||||
|
|
||||||
|
// Only property names are in header now
|
||||||
if (UseMetadata && _propertyNameList is { Count: > 0 })
|
if (UseMetadata && _propertyNameList is { Count: > 0 })
|
||||||
{
|
{
|
||||||
size += GetVarUIntSize((uint)_propertyNameList.Count);
|
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
|
var name = _propertyNameList[i];
|
||||||
size += GetVarUIntSize((uint)byteCount) + byteCount;
|
var byteCount = name.Length; // Assume ASCII (common case)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (UseStringInterning && _internedStringList is { Count: > 0 })
|
|
||||||
{
|
|
||||||
size += GetVarUIntSize((uint)_internedStringList.Count);
|
|
||||||
foreach (var value in _internedStringList)
|
|
||||||
{
|
|
||||||
var byteCount = value.Length; // Assume ASCII for estimation
|
|
||||||
size += GetVarUIntSize((uint)byteCount) + byteCount;
|
size += GetVarUIntSize((uint)byteCount) + byteCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1006,122 +902,105 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
public void WriteHeaderPlaceholder()
|
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;
|
_headerPosition = _position;
|
||||||
_position += 2;
|
_position += UseStringInterning ? 6 : 2;
|
||||||
_estimatedHeaderSize = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reserves space for header based on estimation. Call after metadata registration.
|
/// Reserves space for property name table in header.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ReserveHeaderSpace(int estimatedSize)
|
public void ReserveHeaderSpace(int estimatedSize)
|
||||||
{
|
{
|
||||||
if (estimatedSize > 0)
|
if (estimatedSize <= 0) return;
|
||||||
{
|
|
||||||
EnsureCapacity(estimatedSize);
|
EnsureCapacity(estimatedSize);
|
||||||
_estimatedHeaderSize = estimatedSize;
|
_position += estimatedSize;
|
||||||
_position += estimatedSize;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void FinalizeHeaderSections()
|
public void FinalizeHeaderSections()
|
||||||
{
|
{
|
||||||
var hasPropertyNames = UseMetadata && _propertyNameList is { Count: > 0 };
|
var hasPropertyNames = UseMetadata && _propertyNameList is { Count: > 0 };
|
||||||
var hasInternTable = UseStringInterning && _internedStringList is { Count: > 0 };
|
var hasInternTable = UseStringInterning && _internedStringList is { Count: > 0 };
|
||||||
|
|
||||||
// Fast path: no header payload needed
|
|
||||||
if (!hasPropertyNames && !hasInternTable)
|
|
||||||
{
|
|
||||||
// Write header flags only
|
|
||||||
byte flags = BinaryTypeCode.HeaderFlagsBase;
|
|
||||||
if (UseReferenceHandling)
|
|
||||||
flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
|
|
||||||
|
|
||||||
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
|
|
||||||
_buffer[_headerPosition + 1] = flags;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate actual header size using cached UTF8 bytes
|
|
||||||
var actualSize = 0;
|
|
||||||
if (hasPropertyNames)
|
|
||||||
{
|
|
||||||
actualSize += GetVarUIntSize((uint)_propertyNameList!.Count);
|
|
||||||
foreach (var name in _propertyNameList)
|
|
||||||
{
|
|
||||||
var byteCount = System.Text.Ascii.IsValid(name) ? name.Length : Utf8NoBom.GetByteCount(name);
|
|
||||||
actualSize += GetVarUIntSize((uint)byteCount) + byteCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasInternTable)
|
|
||||||
{
|
|
||||||
actualSize += GetVarUIntSize((uint)_internedStringList!.Count);
|
|
||||||
// Use cached UTF8 byte lengths
|
|
||||||
for (var i = 0; i < _internedStringUtf8!.Count; i++)
|
|
||||||
{
|
|
||||||
var utf8Bytes = _internedStringUtf8[i];
|
|
||||||
actualSize += GetVarUIntSize((uint)utf8Bytes.Length) + utf8Bytes.Length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var bodyStart = _headerPosition + 2 + _estimatedHeaderSize;
|
|
||||||
var bodyLength = _position - bodyStart;
|
|
||||||
|
|
||||||
// Shift body if needed
|
|
||||||
if (actualSize != _estimatedHeaderSize && bodyLength > 0)
|
|
||||||
{
|
|
||||||
var delta = actualSize - _estimatedHeaderSize;
|
|
||||||
if (delta > 0)
|
|
||||||
{
|
|
||||||
EnsureCapacity(delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
var newBodyStart = _headerPosition + 2 + actualSize;
|
|
||||||
if (delta != 0)
|
|
||||||
{
|
|
||||||
Array.Copy(_buffer, bodyStart, _buffer, newBodyStart, bodyLength);
|
|
||||||
_position += delta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write header payload directly to buffer using cached UTF8 bytes
|
|
||||||
var headerPos = _headerPosition + 2;
|
|
||||||
|
|
||||||
|
// Calculate property names header size (strings go to footer now)
|
||||||
|
var headerPayloadSize = 0;
|
||||||
if (hasPropertyNames)
|
if (hasPropertyNames)
|
||||||
{
|
{
|
||||||
headerPos = WriteVarUIntAt(headerPos, (uint)_propertyNameList!.Count);
|
headerPayloadSize += GetVarUIntSize((uint)_propertyNameList!.Count);
|
||||||
foreach (var name in _propertyNameList)
|
for (var i = 0; i < _propertyNameList.Count; i++)
|
||||||
{
|
{
|
||||||
|
var name = _propertyNameList[i];
|
||||||
|
var byteCount = Ascii.IsValid(name) ? name.Length : Utf8NoBom.GetByteCount(name);
|
||||||
|
headerPayloadSize += GetVarUIntSize((uint)byteCount) + byteCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write property names to header if needed
|
||||||
|
var headerPayloadStart = _headerPosition + (UseStringInterning ? 6 : 2);
|
||||||
|
if (hasPropertyNames)
|
||||||
|
{
|
||||||
|
var headerPos = headerPayloadStart;
|
||||||
|
headerPos = WriteVarUIntAt(headerPos, (uint)_propertyNameList!.Count);
|
||||||
|
for (var i = 0; i < _propertyNameList.Count; i++)
|
||||||
|
{
|
||||||
|
var name = _propertyNameList[i];
|
||||||
headerPos = WriteStringAtOptimized(headerPos, name);
|
headerPos = WriteStringAtOptimized(headerPos, name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Footer-based string interning: write strings at the end
|
||||||
|
var footerPosition = 0;
|
||||||
if (hasInternTable)
|
if (hasInternTable)
|
||||||
{
|
{
|
||||||
headerPos = WriteVarUIntAt(headerPos, (uint)_internedStringList!.Count);
|
footerPosition = _position;
|
||||||
// Use cached UTF8 bytes - no re-encoding needed!
|
WriteFooterStrings();
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write header flags
|
// Write header
|
||||||
byte flags2 = BinaryTypeCode.HeaderFlagsBase;
|
var flags = BinaryTypeCode.HeaderFlagsBase;
|
||||||
if (hasPropertyNames)
|
if (hasPropertyNames)
|
||||||
flags2 |= BinaryTypeCode.HeaderFlag_Metadata;
|
flags |= BinaryTypeCode.HeaderFlag_Metadata;
|
||||||
if (UseReferenceHandling)
|
// Encode ReferenceHandlingMode using separate bits
|
||||||
flags2 |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
|
if (ReferenceHandling == ReferenceHandlingMode.OnlyId)
|
||||||
if (hasInternTable)
|
flags |= BinaryTypeCode.HeaderFlag_RefHandling_OnlyId;
|
||||||
flags2 |= BinaryTypeCode.HeaderFlag_StringInternTable;
|
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] = AcBinarySerializerOptions.FormatVersion;
|
||||||
_buffer[_headerPosition + 1] = flags2;
|
_buffer[_headerPosition + 1] = flags;
|
||||||
|
|
||||||
|
// Always write footer position if string interning is enabled in options
|
||||||
|
// (even if there's no actual interned data - footer position will be 0)
|
||||||
|
if (UseStringInterning)
|
||||||
|
{
|
||||||
|
Unsafe.WriteUnaligned(ref _buffer[_headerPosition + 2], footerPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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];
|
||||||
|
WriteVarUInt((uint)utf8Bytes.Length);
|
||||||
|
WriteBytes(utf8Bytes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -1131,10 +1010,10 @@ public static partial class AcBinarySerializer
|
||||||
private int WriteStringAtOptimized(int pos, string value)
|
private int WriteStringAtOptimized(int pos, string value)
|
||||||
{
|
{
|
||||||
// Fast path for ASCII strings
|
// Fast path for ASCII strings
|
||||||
if (System.Text.Ascii.IsValid(value))
|
if (Ascii.IsValid(value))
|
||||||
{
|
{
|
||||||
pos = WriteVarUIntAt(pos, (uint)value.Length);
|
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;
|
return pos + value.Length;
|
||||||
}
|
}
|
||||||
// Standard path for multi-byte UTF8
|
// Standard path for multi-byte UTF8
|
||||||
|
|
@ -1173,7 +1052,7 @@ public static partial class AcBinarySerializer
|
||||||
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
//public bool TrackForScanningWithIId(object obj, BinarySerializeTypeMetadata metadata, out int existingRefId)
|
//public bool TrackForScanningWithIId(object obj, BinarySerializeTypeMetadata metadata, out int existingRefId)
|
||||||
//{
|
//{
|
||||||
// if (!UseReferenceHandling)
|
// if (!ReferenceHandling)
|
||||||
// {
|
// {
|
||||||
// existingRefId = 0;
|
// existingRefId = 0;
|
||||||
// return true; // No tracking needed
|
// return true; // No tracking needed
|
||||||
|
|
@ -1239,7 +1118,7 @@ public static partial class AcBinarySerializer
|
||||||
var length = value.Length;
|
var length = value.Length;
|
||||||
EnsureCapacity(1 + length);
|
EnsureCapacity(1 + length);
|
||||||
_buffer[_position++] = BinaryTypeCode.EncodeFixStr(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;
|
_position += length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using static AyCode.Core.Helpers.JsonUtilities;
|
using static AyCode.Core.Helpers.JsonUtilities;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Binaries;
|
namespace AyCode.Core.Serializers.Binaries;
|
||||||
|
|
@ -175,10 +176,12 @@ public static partial class AcBinarySerializer
|
||||||
var context = BinarySerializationContextPool.Get(options);
|
var context = BinarySerializationContextPool.Get(options);
|
||||||
context.WriteHeaderPlaceholder();
|
context.WriteHeaderPlaceholder();
|
||||||
|
|
||||||
// Single-pass serialization - no scan phase needed!
|
// Single-pass serialization with footer-based string interning
|
||||||
// Reference tracking happens inline via TryTrack during WriteObject
|
// - 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();
|
var estimatedHeaderSize = context.EstimateHeaderPayloadSize();
|
||||||
context.ReserveHeaderSpace(estimatedHeaderSize);
|
context.ReserveHeaderSpace(estimatedHeaderSize);
|
||||||
|
|
||||||
|
|
@ -639,7 +642,7 @@ public static partial class AcBinarySerializer
|
||||||
var metadata = wrapper.Metadata;
|
var metadata = wrapper.Metadata;
|
||||||
|
|
||||||
// Single-pass reference tracking
|
// Single-pass reference tracking
|
||||||
if (context.UseReferenceHandling)
|
if (context.ReferenceHandling != ReferenceHandlingMode.None)
|
||||||
{
|
{
|
||||||
switch (metadata.IdAccessorType)
|
switch (metadata.IdAccessorType)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -27,14 +27,13 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
||||||
public static readonly AcBinarySerializerOptions Default = new();
|
public static readonly AcBinarySerializerOptions Default = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// Use when deserializer knows the exact type structure.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly AcBinarySerializerOptions FastMode = new()
|
public static readonly AcBinarySerializerOptions FastMode = new()
|
||||||
{
|
{
|
||||||
UseMetadata = false,
|
|
||||||
UseStringInterning = false,
|
UseStringInterning = false,
|
||||||
UseReferenceHandling = false
|
ReferenceHandling = ReferenceHandlingMode.None
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -43,9 +42,8 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
||||||
public static readonly AcBinarySerializerOptions ShallowCopy = new()
|
public static readonly AcBinarySerializerOptions ShallowCopy = new()
|
||||||
{
|
{
|
||||||
MaxDepth = 0,
|
MaxDepth = 0,
|
||||||
UseMetadata = false,
|
|
||||||
UseStringInterning = false,
|
UseStringInterning = false,
|
||||||
UseReferenceHandling = false
|
ReferenceHandling = ReferenceHandlingMode.None
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -81,11 +79,11 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether to include metadata header with property names.
|
/// Whether to include metadata header with property names.
|
||||||
/// When enabled, property names are stored once and referenced by index.
|
/// NOTE: Currently unused - deserializer uses ordered property indices, not names.
|
||||||
/// Improves deserialization speed and allows schema evolution.
|
/// Kept for potential future schema evolution support.
|
||||||
/// Default: true
|
/// Default: false (no overhead)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool UseMetadata { get; init; } = true;
|
public bool UseMetadata { get; init; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether to intern repeated strings.
|
/// Whether to intern repeated strings.
|
||||||
|
|
@ -140,7 +138,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
||||||
/// Creates options without reference handling.
|
/// Creates options without reference handling.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static AcBinarySerializerOptions WithoutReferenceHandling() => new() { UseReferenceHandling = false };
|
public static AcBinarySerializerOptions WithoutReferenceHandling() => new() { ReferenceHandling = ReferenceHandlingMode.None };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates options without metadata (faster but less flexible).
|
/// 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
|
// 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
|
// 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 HeaderFlagsBase = 48; // Base value for flag-based headers (0x30)
|
||||||
public const byte HeaderFlag_Metadata = 0x01;
|
public const byte HeaderFlag_Metadata = 0x01; // Bit 0: property metadata included
|
||||||
public const byte HeaderFlag_ReferenceHandling = 0x02;
|
// Reference handling uses 2 separate bits:
|
||||||
public const byte HeaderFlag_StringInternTable = 0x04; // String intern pool preloaded in header
|
// 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)
|
// 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 Int32Tiny = 192; // -16 to 63 stored in single byte (value = code - 192 - 16)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers;
|
namespace AyCode.Core.Serializers;
|
||||||
|
|
||||||
|
|
@ -40,9 +41,9 @@ public abstract class DeserializationContextBase<TMetadata> : AcSerializerContex
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resets deserialization-specific state. Called by derived classes.
|
/// Resets deserialization-specific state. Called by derived classes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override void Reset()
|
public override void Reset(in AcSerializerOptions options)
|
||||||
{
|
{
|
||||||
base.Reset();
|
base.Reset(options);
|
||||||
// Future: Reset deserialization-specific state
|
// Future: Reset deserialization-specific state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,11 @@ public static partial class AcJsonDeserializer
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static void Return(DeserializationContext context)
|
public static void Return(DeserializationContext context, in AcSerializerOptions options)
|
||||||
{
|
{
|
||||||
if (Pool.Count < MaxPoolSize)
|
if (Pool.Count < MaxPoolSize)
|
||||||
{
|
{
|
||||||
context.Clear();
|
context.Clear(null);
|
||||||
Pool.Enqueue(context);
|
Pool.Enqueue(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -52,8 +52,6 @@ public static partial class AcJsonDeserializer
|
||||||
private List<PropertyToResolve>? _propertiesToResolve;
|
private List<PropertyToResolve>? _propertiesToResolve;
|
||||||
|
|
||||||
public bool IsMergeMode { get; set; }
|
public bool IsMergeMode { get; set; }
|
||||||
public bool UseReferenceHandling { get; private set; }
|
|
||||||
public byte MaxDepth { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Chain reference tracker for maintaining object identity across chain operations.
|
/// Chain reference tracker for maintaining object identity across chain operations.
|
||||||
|
|
@ -77,27 +75,28 @@ public static partial class AcJsonDeserializer
|
||||||
Reset(options);
|
Reset(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public new void Reset(in AcJsonSerializerOptions options)
|
public override void Reset(in AcSerializerOptions? options)
|
||||||
{
|
{
|
||||||
UseReferenceHandling = options.UseReferenceHandling;
|
|
||||||
MaxDepth = options.MaxDepth;
|
|
||||||
IsMergeMode = false;
|
IsMergeMode = false;
|
||||||
ChainTracker = null;
|
ChainTracker = null;
|
||||||
_refTracker.Reset();
|
_refTracker.Reset();
|
||||||
|
|
||||||
|
base.Reset(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public new void Clear()
|
public void Clear(in AcSerializerOptions? options)
|
||||||
{
|
{
|
||||||
base.Reset();
|
|
||||||
_refTracker.Reset();
|
_refTracker.Reset();
|
||||||
_propertiesToResolve?.Clear();
|
_propertiesToResolve?.Clear();
|
||||||
ChainTracker = null;
|
ChainTracker = null;
|
||||||
|
|
||||||
|
Reset(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void RegisterObject(int id, object obj)
|
public void RegisterObject(int id, object obj)
|
||||||
{
|
{
|
||||||
if (!UseReferenceHandling) return;
|
if (ReferenceHandling == ReferenceHandlingMode.None) return;
|
||||||
_refTracker.RegisterObject(id, obj);
|
_refTracker.RegisterObject(id, obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ public static partial class AcJsonDeserializer
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!options.UseReferenceHandling)
|
if (options.ReferenceHandling == ReferenceHandlingMode.None)
|
||||||
{
|
{
|
||||||
var reader = new Utf8JsonReader(utf8Json, new JsonReaderOptions { MaxDepth = options.MaxDepth });
|
var reader = new Utf8JsonReader(utf8Json, new JsonReaderOptions { MaxDepth = options.MaxDepth });
|
||||||
if (!reader.Read()) return default;
|
if (!reader.Read()) return default;
|
||||||
|
|
@ -77,7 +77,7 @@ public static partial class AcJsonDeserializer
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
JsonDeserializationContextPool.Return(context);
|
JsonDeserializationContextPool.Return(context, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (AcJsonDeserializationException) { throw; }
|
catch (AcJsonDeserializationException) { throw; }
|
||||||
|
|
@ -121,7 +121,7 @@ public static partial class AcJsonDeserializer
|
||||||
|
|
||||||
ValidateJson(json, targetType);
|
ValidateJson(json, targetType);
|
||||||
|
|
||||||
if (!options.UseReferenceHandling)
|
if (options.ReferenceHandling == ReferenceHandlingMode.None)
|
||||||
{
|
{
|
||||||
return DeserializeWithUtf8Reader<T>(json, options.MaxDepth);
|
return DeserializeWithUtf8Reader<T>(json, options.MaxDepth);
|
||||||
}
|
}
|
||||||
|
|
@ -136,7 +136,7 @@ public static partial class AcJsonDeserializer
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
JsonDeserializationContextPool.Return(context);
|
JsonDeserializationContextPool.Return(context, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (AcJsonDeserializationException) { throw; }
|
catch (AcJsonDeserializationException) { throw; }
|
||||||
|
|
@ -186,7 +186,7 @@ public static partial class AcJsonDeserializer
|
||||||
ValidateJson(json, targetType);
|
ValidateJson(json, targetType);
|
||||||
|
|
||||||
// Fast path for no reference handling
|
// Fast path for no reference handling
|
||||||
if (!options.UseReferenceHandling)
|
if (options.ReferenceHandling == ReferenceHandlingMode.None)
|
||||||
{
|
{
|
||||||
return DeserializeWithUtf8ReaderNonGeneric(json, targetType, options.MaxDepth);
|
return DeserializeWithUtf8ReaderNonGeneric(json, targetType, options.MaxDepth);
|
||||||
}
|
}
|
||||||
|
|
@ -203,7 +203,7 @@ public static partial class AcJsonDeserializer
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
JsonDeserializationContextPool.Return(context);
|
JsonDeserializationContextPool.Return(context, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (AcJsonDeserializationException) { throw; }
|
catch (AcJsonDeserializationException) { throw; }
|
||||||
|
|
@ -269,7 +269,7 @@ public static partial class AcJsonDeserializer
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Fast path for no reference handling
|
// Fast path for no reference handling
|
||||||
if (!options.UseReferenceHandling)
|
if (options.ReferenceHandling == ReferenceHandlingMode.None)
|
||||||
{
|
{
|
||||||
var reader = new Utf8JsonReader(utf8Json, new JsonReaderOptions { MaxDepth = options.MaxDepth });
|
var reader = new Utf8JsonReader(utf8Json, new JsonReaderOptions { MaxDepth = options.MaxDepth });
|
||||||
if (!reader.Read()) return null;
|
if (!reader.Read()) return null;
|
||||||
|
|
@ -288,7 +288,7 @@ public static partial class AcJsonDeserializer
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
JsonDeserializationContextPool.Return(context);
|
JsonDeserializationContextPool.Return(context, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (AcJsonDeserializationException) { throw; }
|
catch (AcJsonDeserializationException) { throw; }
|
||||||
|
|
@ -363,7 +363,7 @@ public static partial class AcJsonDeserializer
|
||||||
ValidateJson(json, targetType);
|
ValidateJson(json, targetType);
|
||||||
|
|
||||||
// Fast path for no reference handling - use Utf8JsonReader streaming
|
// Fast path for no reference handling - use Utf8JsonReader streaming
|
||||||
if (!options.UseReferenceHandling)
|
if (options.ReferenceHandling == ReferenceHandlingMode.None)
|
||||||
{
|
{
|
||||||
var firstChar = json[0];
|
var firstChar = json[0];
|
||||||
|
|
||||||
|
|
@ -418,7 +418,7 @@ public static partial class AcJsonDeserializer
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
JsonDeserializationContextPool.Return(context);
|
JsonDeserializationContextPool.Return(context, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (AcJsonDeserializationException) { throw; }
|
catch (AcJsonDeserializationException) { throw; }
|
||||||
|
|
@ -508,7 +508,7 @@ public static partial class AcJsonDeserializer
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
JsonDeserializationContextPool.Return(context);
|
JsonDeserializationContextPool.Return(context, options);
|
||||||
doc.Dispose();
|
doc.Dispose();
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
@ -621,7 +621,7 @@ public static partial class AcJsonDeserializer
|
||||||
|
|
||||||
if (_context != null)
|
if (_context != null)
|
||||||
{
|
{
|
||||||
JsonDeserializationContextPool.Return(_context);
|
JsonDeserializationContextPool.Return(_context, null);
|
||||||
_context = null;
|
_context = null;
|
||||||
}
|
}
|
||||||
_document?.Dispose();
|
_document?.Dispose();
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,6 @@ public static partial class AcJsonSerializer
|
||||||
// Use shared reference tracker from AcSerializerCommon
|
// Use shared reference tracker from AcSerializerCommon
|
||||||
private readonly AcSerializerCommon.SerializationReferenceTracker _refTracker = new();
|
private readonly AcSerializerCommon.SerializationReferenceTracker _refTracker = new();
|
||||||
|
|
||||||
public bool UseReferenceHandling { get; private set; }
|
|
||||||
public byte MaxDepth { get; private set; }
|
|
||||||
|
|
||||||
private static readonly JsonWriterOptions WriterOptions = new()
|
private static readonly JsonWriterOptions WriterOptions = new()
|
||||||
{
|
{
|
||||||
Indented = false,
|
Indented = false,
|
||||||
|
|
@ -65,16 +62,16 @@ public static partial class AcJsonSerializer
|
||||||
protected override Func<Type, JsonSerializeTypeMetadata> MetadataFactory
|
protected override Func<Type, JsonSerializeTypeMetadata> MetadataFactory
|
||||||
=> static t => new JsonSerializeTypeMetadata(t);
|
=> 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();
|
_refTracker.Reset();
|
||||||
|
|
||||||
if (UseReferenceHandling)
|
if (ReferenceHandling != ReferenceHandlingMode.None)
|
||||||
{
|
{
|
||||||
_refTracker.EnsureInitialized();
|
_refTracker.EnsureInitialized();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
base.Reset(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ public static partial class AcJsonSerializer
|
||||||
var context = SerializationContextPool.Get(options);
|
var context = SerializationContextPool.Get(options);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (options.UseReferenceHandling)
|
if (options.ReferenceHandling != ReferenceHandlingMode.None)
|
||||||
ScanReferences(actualValue, context, 0);
|
ScanReferences(actualValue, context, 0);
|
||||||
|
|
||||||
WriteValue(actualValue, context, 0);
|
WriteValue(actualValue, context, 0);
|
||||||
|
|
@ -182,7 +182,7 @@ public static partial class AcJsonSerializer
|
||||||
var metadata = wrapper.Metadata;
|
var metadata = wrapper.Metadata;
|
||||||
|
|
||||||
// Use IId-aware reference handling
|
// 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.WriteStartObject();
|
||||||
writer.WriteString(RefPropertyEncoded, refId.ToString(CultureInfo.InvariantCulture));
|
writer.WriteString(RefPropertyEncoded, refId.ToString(CultureInfo.InvariantCulture));
|
||||||
|
|
@ -192,7 +192,7 @@ public static partial class AcJsonSerializer
|
||||||
|
|
||||||
writer.WriteStartObject();
|
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));
|
writer.WriteString(IdPropertyEncoded, id.ToString(CultureInfo.InvariantCulture));
|
||||||
context.MarkAsWrittenForIId(value, metadata, id);
|
context.MarkAsWrittenForIId(value, metadata, id);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,27 @@ public enum AcSerializerType : byte
|
||||||
Toon = 2,
|
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>
|
/// <summary>
|
||||||
/// Delegate for custom property mapping during cross-type deserialization/population.
|
/// Delegate for custom property mapping during cross-type deserialization/population.
|
||||||
/// Enables mapping between different class hierarchies or renamed properties.
|
/// Enables mapping between different class hierarchies or renamed properties.
|
||||||
|
|
@ -23,10 +44,10 @@ public abstract class AcSerializerOptions
|
||||||
public abstract AcSerializerType SerializerType { get; init; }
|
public abstract AcSerializerType SerializerType { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether to use $id/$ref reference handling for circular references.
|
/// Reference handling mode for circular/shared references.
|
||||||
/// Default: true
|
/// Default: OnlyId (handles IId objects)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool UseReferenceHandling { get; init; } = true;
|
public ReferenceHandlingMode ReferenceHandling { get; init; } = ReferenceHandlingMode.OnlyId;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maximum depth for serialization/deserialization.
|
/// Maximum depth for serialization/deserialization.
|
||||||
|
|
@ -69,7 +90,7 @@ public sealed class AcJsonSerializerOptions : AcSerializerOptions
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Options for shallow serialization (root level only, no references).
|
/// Options for shallow serialization (root level only, no references).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly AcJsonSerializerOptions ShallowCopy = new() { MaxDepth = 0, UseReferenceHandling = false };
|
public static readonly AcJsonSerializerOptions ShallowCopy = new() { MaxDepth = 0, ReferenceHandling = ReferenceHandlingMode.None };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates options with specified max depth.
|
/// Creates options with specified max depth.
|
||||||
|
|
@ -79,5 +100,5 @@ public sealed class AcJsonSerializerOptions : AcSerializerOptions
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates options without reference handling.
|
/// Creates options without reference handling.
|
||||||
/// </summary>
|
/// </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;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
@ -102,9 +103,9 @@ public abstract class SerializationContextBase<TMetadata> : AcSerializerContextB
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resets serialization-specific state. Called by derived classes.
|
/// Resets serialization-specific state. Called by derived classes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override void Reset()
|
public override void Reset(in AcSerializerOptions options)
|
||||||
{
|
{
|
||||||
base.Reset();
|
base.Reset(options);
|
||||||
// Future: Reset serialization-specific state
|
// Future: Reset serialization-specific state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Collections;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using static AyCode.Core.Helpers.JsonUtilities;
|
using static AyCode.Core.Helpers.JsonUtilities;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Toons;
|
namespace AyCode.Core.Serializers.Toons;
|
||||||
|
|
@ -53,7 +54,7 @@ public static partial class AcToonSerializer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for reference
|
// 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}");
|
context.Write($"@ref:{refId}");
|
||||||
return;
|
return;
|
||||||
|
|
@ -227,7 +228,7 @@ public static partial class AcToonSerializer
|
||||||
var metadata = GetTypeMetadata(type);
|
var metadata = GetTypeMetadata(type);
|
||||||
|
|
||||||
// Write reference ID if this is a multi-referenced object
|
// 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.Write($"@{refId} ");
|
||||||
context.MarkAsWritten(value, refId);
|
context.MarkAsWritten(value, refId);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ using System.Collections.Generic;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
|
|
||||||
//using ReferenceEqualityComparer = AyCode.Core.Serializers.ReferenceEqualityComparer;
|
//using ReferenceEqualityComparer = AyCode.Core.Serializers.ReferenceEqualityComparer;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Toons;
|
namespace AyCode.Core.Serializers.Toons;
|
||||||
|
|
@ -58,8 +60,6 @@ public static partial class AcToonSerializer
|
||||||
|
|
||||||
public AcToonSerializerOptions Options { get; private set; }
|
public AcToonSerializerOptions Options { get; private set; }
|
||||||
public int CurrentIndentLevel { get; set; }
|
public int CurrentIndentLevel { get; set; }
|
||||||
public bool UseReferenceHandling { get; private set; }
|
|
||||||
public byte MaxDepth { get; private set; }
|
|
||||||
|
|
||||||
public ToonSerializationContext(AcToonSerializerOptions options)
|
public ToonSerializationContext(AcToonSerializerOptions options)
|
||||||
{
|
{
|
||||||
|
|
@ -77,12 +77,10 @@ public static partial class AcToonSerializer
|
||||||
public void Reset(AcToonSerializerOptions options)
|
public void Reset(AcToonSerializerOptions options)
|
||||||
{
|
{
|
||||||
Options = options;
|
Options = options;
|
||||||
UseReferenceHandling = options.UseReferenceHandling;
|
|
||||||
MaxDepth = options.MaxDepth;
|
|
||||||
CurrentIndentLevel = 0;
|
CurrentIndentLevel = 0;
|
||||||
_nextRefId = 1;
|
_nextRefId = 1;
|
||||||
|
|
||||||
if (UseReferenceHandling)
|
if (ReferenceHandling != ReferenceHandlingMode.None)
|
||||||
{
|
{
|
||||||
_scanOccurrences ??= new Dictionary<object, int>(64, ReferenceEqualityComparer.Instance);
|
_scanOccurrences ??= new Dictionary<object, int>(64, ReferenceEqualityComparer.Instance);
|
||||||
_writtenRefs ??= new Dictionary<object, int>(32, ReferenceEqualityComparer.Instance);
|
_writtenRefs ??= new Dictionary<object, int>(32, ReferenceEqualityComparer.Instance);
|
||||||
|
|
@ -90,6 +88,7 @@ public static partial class AcToonSerializer
|
||||||
}
|
}
|
||||||
|
|
||||||
_registeredTypes ??= new HashSet<Type>(16);
|
_registeredTypes ??= new HashSet<Type>(16);
|
||||||
|
base.Reset(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Collections.Concurrent;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
using static AyCode.Core.Helpers.JsonUtilities;
|
using static AyCode.Core.Helpers.JsonUtilities;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Toons;
|
namespace AyCode.Core.Serializers.Toons;
|
||||||
|
|
@ -50,7 +51,7 @@ public static partial class AcToonSerializer
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Reference scanning if needed
|
// Reference scanning if needed
|
||||||
if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(type))
|
if (context.ReferenceHandling != ReferenceHandlingMode.None && !IsPrimitiveOrStringFast(type))
|
||||||
{
|
{
|
||||||
ScanReferences(value, context, 0);
|
ScanReferences(value, context, 0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,7 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions
|
||||||
UseIndentation = true,
|
UseIndentation = true,
|
||||||
OmitDefaultValues = true,
|
OmitDefaultValues = true,
|
||||||
WriteTypeNames = false,
|
WriteTypeNames = false,
|
||||||
UseReferenceHandling = false
|
ReferenceHandling = ReferenceHandlingMode.None
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -210,5 +210,5 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates options without reference handling (faster, no circular reference support).
|
/// Creates options without reference handling (faster, no circular reference support).
|
||||||
/// </summary>
|
/// </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
|
protected override AcSerializerOptions SerializerOption { get; } = new AcBinarySerializerOptions
|
||||||
{
|
{
|
||||||
UseReferenceHandling = false
|
ReferenceHandling = ReferenceHandlingMode.None
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ namespace AyCode.Services.Server.Tests.SignalRs.SignalRDatasources;
|
||||||
[TestClass]
|
[TestClass]
|
||||||
public class SignalRDataSourceTests_List_Binary_NoRef : SignalRDataSourceTestBase<TestOrderItemListDataSource, List<TestOrderItem>>
|
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)
|
protected override TestOrderItemListDataSource CreateDataSource(TestableSignalRClient2 client, SignalRCrudTags crudTags)
|
||||||
=> new(client, crudTags);
|
=> new(client, crudTags);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue