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:
Loretta 2026-01-23 10:50:19 +01:00
parent 905b1c404d
commit cdf3cf34f8
23 changed files with 971 additions and 468 deletions

View File

@ -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()
}; };

View File

@ -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>

View File

@ -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
} }

View File

@ -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)}");
} }
} }

View File

@ -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!

View File

@ -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

View File

@ -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)]

View File

@ -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;
} }

View File

@ -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)
{ {

View File

@ -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)

View File

@ -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
} }

View File

@ -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);
} }

View File

@ -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();

View File

@ -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()

View File

@ -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);

View File

@ -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 };
} }

View File

@ -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
} }

View File

@ -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);

View File

@ -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()

View File

@ -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);
} }

View File

@ -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 };
} }

View File

@ -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
}; };
} }

View File

@ -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);