1237 lines
62 KiB
C#
1237 lines
62 KiB
C#
using AyCode.Core.Compression;
|
||
using AyCode.Core.Serializers.Binaries;
|
||
using AyCode.Core.Tests.TestModels;
|
||
using MemoryPack;
|
||
using MessagePack;
|
||
using MessagePack.Resolvers;
|
||
using Microsoft.Extensions.Options;
|
||
using System.Buffers;
|
||
using System.Diagnostics;
|
||
using System.Runtime.CompilerServices;
|
||
using System.Text;
|
||
using System.Text.Json;
|
||
|
||
namespace AyCode.Core.Serializers.Console;
|
||
|
||
/// <summary>
|
||
/// Comprehensive benchmark application for all serializers.
|
||
/// Compares: AcBinary (all options), MemoryPack, MessagePack, Newtonsoft.Json, System.Text.Json
|
||
///
|
||
/// Usage:
|
||
/// dotnet run # Run all benchmarks
|
||
/// dotnet run -- quick # Quick mode (fewer iterations)
|
||
/// dotnet run -- serialize # Serialize only
|
||
/// dotnet run -- deserialize # Deserialize only
|
||
/// </summary>
|
||
public static class Program
|
||
{
|
||
private const string ResultsDirectory = @"H:\Applications\Aycode\Source\AyCode.Core\Test_Benchmark_Results\Benchmark";
|
||
|
||
#if DEBUG
|
||
private const string BuildConfiguration = "Debug";
|
||
#else
|
||
private const string BuildConfiguration = "Release";
|
||
#endif
|
||
|
||
// Serializer name constants
|
||
private const string SerializerMessagePack = "MessagePack";
|
||
private const string SerializerAcBinaryDefault = "AcBinary (Default)";
|
||
private const string SerializerAcBinaryDefaultNoSgen = "AcBinary (Def, NoSgen)";
|
||
private const string SerializerAcBinaryNoRef = "AcBinary (NoRef)";
|
||
private const string SerializerAcBinaryFastMode = "AcBinary (FastMode)";
|
||
private const string SerializerAcBinaryFastNoSgen = "AcBinary (Fast, NoSgen)";
|
||
private const string SerializerAcBinaryNoIntern = "AcBinary (NoIntern)";
|
||
private const string SerializerMemoryPack = "MemoryPack";
|
||
//private const string SerializerAcBinaryBufferWriter = "AcBinary (BufferWriter)";
|
||
//private const string SerializerSystemTextJson = "System.Text.Json";
|
||
|
||
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
|
||
|
||
#if DEBUG
|
||
private static int WarmupIterations = 0;
|
||
private static int TestIterations = 1;
|
||
private static int BenchmarkSamples = 1; // Debug: single sample, fast iteration
|
||
#else
|
||
private static int WarmupIterations = 5000;
|
||
private static int TestIterations = 1000;
|
||
private static int BenchmarkSamples = 5; // Release: 5-sample median for stability (~±5% variance vs. ~±15% single-sample)
|
||
|
||
//private static int WarmupIterations = 5000;
|
||
//private static int TestIterations = 2000;
|
||
#endif
|
||
|
||
public static void Main(string[] args)
|
||
{
|
||
// Set console encoding to UTF-8 for proper Unicode character display
|
||
System.Console.OutputEncoding = Encoding.UTF8;
|
||
|
||
// Setup validation — abort BEFORE any benchmark logic if MemoryPack baseline is invalid.
|
||
// Done early so user is told immediately, not after warmup.
|
||
ValidateMemoryPackSetup();
|
||
|
||
// Determine layer (which test data to run) and opMode (ser/des/all).
|
||
// CLI args take precedence; if no args, show interactive menu.
|
||
string layer;
|
||
string opMode = "all";
|
||
|
||
if (args.Length == 0)
|
||
{
|
||
var selection = ShowInteractiveMenu();
|
||
if (selection == null) return; // user pressed Q
|
||
layer = selection;
|
||
}
|
||
else
|
||
{
|
||
var arg = args[0].ToLower();
|
||
|
||
// Profiler mode: warmup only, then exit (for memory profiler analysis)
|
||
if (arg == "profiler")
|
||
{
|
||
RunProfilerMode();
|
||
return;
|
||
}
|
||
|
||
// Quick mode: short warmup, few iterations, small sample count
|
||
if (arg == "quick")
|
||
{
|
||
WarmupIterations = 5;
|
||
TestIterations = 100;
|
||
BenchmarkSamples = 3;
|
||
layer = "all";
|
||
}
|
||
else if (arg is "core" or "comprehensive" or "edge" or "all")
|
||
{
|
||
layer = arg;
|
||
}
|
||
else if (arg is "ser" or "serialize")
|
||
{
|
||
opMode = "serialize";
|
||
layer = "all";
|
||
}
|
||
else if (arg is "des" or "deserialize")
|
||
{
|
||
opMode = "deserialize";
|
||
layer = "all";
|
||
}
|
||
else
|
||
{
|
||
// Backwards compat: unknown arg → treat as layer keyword
|
||
layer = arg;
|
||
}
|
||
}
|
||
|
||
System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════╗");
|
||
System.Console.WriteLine("║ COMPREHENSIVE SERIALIZER BENCHMARK SUITE ║");
|
||
System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════╝");
|
||
var allResults = new List<BenchmarkResult>();
|
||
var allTestDataSets = BenchmarkTestDataProvider.CreateTestDataSets();
|
||
var testDataSets = FilterByLayer(allTestDataSets, layer);
|
||
|
||
System.Console.WriteLine($"Layer: {layer} | OpMode: {opMode} | Iterations: {TestIterations} | Warmup: {WarmupIterations} | Samples: {BenchmarkSamples} (median)");
|
||
System.Console.WriteLine($"Build: {BuildConfiguration} | .NET: {Environment.Version} | Test Type: {testDataSets.FirstOrDefault()?.TypeName ?? "unknown"} | Test Cells: {testDataSets.Count}/{allTestDataSets.Count}");
|
||
System.Console.WriteLine();
|
||
|
||
foreach (var testData in testDataSets)
|
||
{
|
||
System.Console.WriteLine($"\n{'═'.ToString().PadRight(70, '═')}");
|
||
System.Console.WriteLine($"TEST DATA: {testData.DisplayName}");
|
||
System.Console.WriteLine($"{'═'.ToString().PadRight(70, '═')}");
|
||
|
||
var results = RunBenchmarksForTestData(testData, opMode);
|
||
allResults.AddRange(results);
|
||
}
|
||
|
||
// Print grouped results
|
||
PrintGroupedResults(allResults, testDataSets);
|
||
|
||
// Save results to file
|
||
SaveResults(allResults, testDataSets);
|
||
|
||
System.Console.WriteLine("\n✓ Benchmark complete!");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Profiler mode: warmup only, then EXIT immediately.
|
||
/// Usage: dotnet run -- profiler
|
||
/// </summary>
|
||
private static void RunProfilerMode()
|
||
{
|
||
System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════╗");
|
||
System.Console.WriteLine("║ PROFILER MODE (AcBinary only) ║");
|
||
System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════╝");
|
||
System.Console.WriteLine($"Build: {BuildConfiguration} | .NET: {Environment.Version}");
|
||
System.Console.WriteLine();
|
||
|
||
var order = BenchmarkTestDataProvider.CreateProfilerOrder();
|
||
|
||
var options = AcBinarySerializerOptions.WithoutReferenceHandling;
|
||
options.UseStringInterning = StringInterningMode.None;
|
||
|
||
byte[] bytes = AcBinarySerializer.Serialize(order, options);
|
||
// Warmup (fills caches)
|
||
System.Console.WriteLine("Warming up (1000 iterations)...");
|
||
for (var i = 0; i < 1000; i++)
|
||
{
|
||
_ = AcBinarySerializer.Serialize(order, options);
|
||
_ = AcBinaryDeserializer.Deserialize<TestOrder>(bytes);
|
||
}
|
||
|
||
Thread.Sleep(2000);
|
||
System.Console.WriteLine("Warmup complete. Caches are now populated.");
|
||
System.Console.WriteLine();
|
||
|
||
// HOT PATH - this is what the profiler should capture!
|
||
System.Console.WriteLine("Running hot path serialization (1000 iterations for profiling)...");
|
||
for (var i = 0; i < 1000; i++)
|
||
{
|
||
_ = AcBinarySerializer.Serialize(order, options);
|
||
//_ = AcBinaryDeserializer.Deserialize<TestOrder>(bytes);
|
||
}
|
||
|
||
System.Console.WriteLine("Running hot path deserialization (1000 iterations for profiling)...");
|
||
for (var i = 0; i < 1000; i++)
|
||
{
|
||
_ = AcBinaryDeserializer.Deserialize<TestOrder>(bytes);
|
||
}
|
||
|
||
System.Console.WriteLine("Hot path complete.");
|
||
System.Console.WriteLine();
|
||
|
||
System.Console.WriteLine(">>> ATTACH MEMORY PROFILER NOW <<<");
|
||
System.Console.WriteLine("Press any key to exit...");
|
||
System.Console.ReadKey(intercept: true);
|
||
System.Console.WriteLine();
|
||
System.Console.WriteLine("✓ Profiler mode complete. Exiting now.");
|
||
}
|
||
|
||
#region Benchmark Execution
|
||
|
||
private static List<BenchmarkResult> RunBenchmarksForTestData(TestDataSet testData, string mode)
|
||
{
|
||
var results = new List<BenchmarkResult>();
|
||
var serializers = CreateSerializers(testData);
|
||
|
||
// Round-trip correctness check — once per (cell × serializer), BEFORE warmup. Aborts the entire benchmark on failure.
|
||
System.Console.WriteLine("Verifying round-trip correctness...");
|
||
foreach (var serializer in serializers)
|
||
{
|
||
if (!serializer.VerifyRoundTrip())
|
||
{
|
||
System.Console.Error.WriteLine($"❌ FATAL: Round-trip verification FAILED for {serializer.Name} on {testData.DisplayName}");
|
||
System.Console.Error.WriteLine("Benchmark numbers from a serializer with broken round-trip would be meaningless. Aborting.");
|
||
Environment.Exit(1);
|
||
}
|
||
}
|
||
System.Console.WriteLine("✓ All serializers passed round-trip verification.");
|
||
|
||
// Warmup all serializers
|
||
System.Console.WriteLine($"Warming up ({WarmupIterations} iterations)...");
|
||
foreach (var serializer in serializers)
|
||
{
|
||
serializer.Warmup(WarmupIterations);
|
||
}
|
||
|
||
// Wait for tiered JIT background compilation to complete
|
||
Thread.Sleep(3000);
|
||
|
||
// Run benchmarks
|
||
System.Console.WriteLine($"Running benchmarks ({TestIterations} iterations × {BenchmarkSamples} samples median)...\n");
|
||
|
||
foreach (var serializer in serializers)
|
||
{
|
||
var result = new BenchmarkResult
|
||
{
|
||
TestDataName = testData.DisplayName, // Use DisplayName for IId% info
|
||
SerializerName = serializer.Name,
|
||
OptionsDescription = serializer.OptionsDescription,
|
||
SerializedSize = serializer.SerializedSize
|
||
};
|
||
|
||
if (mode is "all" or "serialize" or "ser")
|
||
{
|
||
result.SerializeTimeMs = RunTimed(() => serializer.Serialize(), TestIterations);
|
||
// Dedicated alloc-only sample (separate from timing samples; keeps timing pure)
|
||
result.SerializeAllocBytesPerOp = MeasureAllocation(() => serializer.Serialize(), TestIterations);
|
||
}
|
||
|
||
if (mode is "all" or "deserialize" or "des")
|
||
{
|
||
result.DeserializeTimeMs = RunTimed(() => serializer.Deserialize(), TestIterations);
|
||
result.DeserializeAllocBytesPerOp = MeasureAllocation(() => serializer.Deserialize(), TestIterations);
|
||
}
|
||
|
||
results.Add(result);
|
||
PrintResult(result);
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
private static List<ISerializerBenchmark> CreateSerializers(TestDataSet testData)
|
||
{
|
||
var binaryNoInternOption = AcBinarySerializerOptions.Default;
|
||
binaryNoInternOption.UseStringInterning = StringInterningMode.None;
|
||
|
||
var binaryDefaultNoSgenOption = AcBinarySerializerOptions.Default;
|
||
binaryDefaultNoSgenOption.UseGeneratedCode = false;
|
||
|
||
var binaryFastModeNoSgenOption = AcBinarySerializerOptions.FastMode;
|
||
binaryFastModeNoSgenOption.UseGeneratedCode = false;
|
||
|
||
return new List<ISerializerBenchmark>
|
||
{
|
||
|
||
// AcBinary variants
|
||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
|
||
////new AcBinaryBenchmark(testData.Order, binaryFastModeNoSgenOption, SerializerAcBinaryFastNoSgen),
|
||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
|
||
////new AcBinaryBenchmark(testData.Order, binaryDefaultNoSgenOption, SerializerAcBinaryDefaultNoSgen),
|
||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef),
|
||
//new AcBinaryBenchmark(testData.Order, binaryNoInternOption, SerializerAcBinaryNoIntern),
|
||
|
||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
|
||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastNoSgen),
|
||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefault),
|
||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefaultNoSgen),
|
||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoRef),
|
||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoIntern),
|
||
|
||
|
||
// MemoryPack
|
||
new MemoryPackBenchmark(testData.Order, SerializerMemoryPack),
|
||
|
||
// MessagePack
|
||
new MessagePackBenchmark(testData.Order, SerializerMessagePack),
|
||
|
||
// AcBinary BufferWriter
|
||
//new AcBinaryBufferWriterBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryBufferWriter),
|
||
|
||
// System.Text.Json
|
||
//new SystemTextJsonBenchmark(testData.Order, SerializerSystemTextJson)
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// Runs the action <paramref name="iterations"/> times for <see cref="BenchmarkSamples"/> independent samples,
|
||
/// returning the median elapsed time. Multi-sample design reduces single-run variance from ~±15% to ~±5%
|
||
/// by smoothing transient effects (background activity, thermal/turbo state, JIT tier-promotion timing).
|
||
/// When <see cref="BenchmarkSamples"/> <= 1, falls back to single-sample timing (Debug / quick mode).
|
||
/// </summary>
|
||
private static double RunTimed(Action action, int iterations)
|
||
{
|
||
var samples = BenchmarkSamples;
|
||
if (samples <= 1)
|
||
{
|
||
// Single-sample fast path (Debug or trivial run) — no allocation, no sort.
|
||
var sw = Stopwatch.StartNew();
|
||
for (var i = 0; i < iterations; i++) action();
|
||
sw.Stop();
|
||
return sw.Elapsed.TotalMilliseconds;
|
||
}
|
||
|
||
var times = new double[samples];
|
||
for (int s = 0; s < samples; s++)
|
||
{
|
||
var sw = Stopwatch.StartNew();
|
||
for (var i = 0; i < iterations; i++) action();
|
||
sw.Stop();
|
||
times[s] = sw.Elapsed.TotalMilliseconds;
|
||
}
|
||
Array.Sort(times);
|
||
// Median: middle value for odd sample counts, average of two middles for even counts.
|
||
return samples % 2 == 1
|
||
? times[samples / 2]
|
||
: (times[samples / 2 - 1] + times[samples / 2]) / 2.0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Measures per-call allocation in bytes after a clean GC. Single dedicated sample (no median) — keeps timing samples pure.
|
||
/// </summary>
|
||
private static long MeasureAllocation(Action action, int iterations)
|
||
{
|
||
GC.Collect();
|
||
GC.WaitForPendingFinalizers();
|
||
GC.Collect();
|
||
var before = GC.GetAllocatedBytesForCurrentThread();
|
||
for (var i = 0; i < iterations; i++) action();
|
||
var after = GC.GetAllocatedBytesForCurrentThread();
|
||
return (after - before) / iterations;
|
||
}
|
||
|
||
private static readonly JsonSerializerOptions VerifyJsonOpts = new()
|
||
{
|
||
WriteIndented = false,
|
||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
||
ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles
|
||
};
|
||
|
||
/// <summary>
|
||
/// Round-trip equality check: serialize both via System.Text.Json (canonical form) and compare strings.
|
||
/// Slower than property-by-property compare, but universal — works for any object graph without custom comparer.
|
||
/// </summary>
|
||
private static bool DeepEqualsViaJson(object? a, object? b)
|
||
{
|
||
if (a == null && b == null) return true;
|
||
if (a == null || b == null) return false;
|
||
var jsonA = JsonSerializer.Serialize(a, VerifyJsonOpts);
|
||
var jsonB = JsonSerializer.Serialize(b, VerifyJsonOpts);
|
||
return jsonA == jsonB;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Validates MemoryPack setup at startup. Aborts the benchmark if TestOrder is not [MemoryPackable].
|
||
/// Without this attribute, MemoryPack falls back to runtime resolver (slower) — comparison would be INVALID.
|
||
/// </summary>
|
||
private static void ValidateMemoryPackSetup()
|
||
{
|
||
var typesToCheck = new[] { typeof(TestOrder) };
|
||
foreach (var type in typesToCheck)
|
||
{
|
||
var hasAttr = type.GetCustomAttributes(typeof(MemoryPackableAttribute), inherit: true).Any();
|
||
if (!hasAttr)
|
||
{
|
||
System.Console.Error.WriteLine($"❌ FATAL: {type.FullName} is not [MemoryPackable] — MemoryPack would fall back to runtime resolver, comparison is INVALID for SGen-vs-SGen claim.");
|
||
System.Console.Error.WriteLine("Add [MemoryPackable] to the type and any nested types referenced from it.");
|
||
Environment.Exit(1);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Interactive menu shown when no CLI args. Returns the layer keyword (core/comprehensive/edge/all) or null on Quit.
|
||
/// </summary>
|
||
private static string? ShowInteractiveMenu()
|
||
{
|
||
System.Console.WriteLine();
|
||
System.Console.WriteLine("╔══════════════════════════════════════════════════════════╗");
|
||
System.Console.WriteLine("║ AcBinary Benchmark Suite ║");
|
||
System.Console.WriteLine("╚══════════════════════════════════════════════════════════╝");
|
||
System.Console.WriteLine();
|
||
System.Console.WriteLine("Select benchmark layer:");
|
||
System.Console.WriteLine();
|
||
System.Console.WriteLine(" [1] Core — daily iteration");
|
||
System.Console.WriteLine(" [2] Comprehensive — release validation");
|
||
System.Console.WriteLine(" [3] Edge cases — refactor verification");
|
||
System.Console.WriteLine(" [A] All layers");
|
||
System.Console.WriteLine(" [Q] Quit");
|
||
System.Console.Write("\nSelection: ");
|
||
var key = System.Console.ReadKey(intercept: false).KeyChar;
|
||
System.Console.WriteLine();
|
||
return char.ToLower(key) switch
|
||
{
|
||
'1' => "core",
|
||
'2' => "comprehensive",
|
||
'3' => "edge",
|
||
'a' => "all",
|
||
'q' => null,
|
||
_ => "core"
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// Filters test data sets by layer keyword. Layered approach lets you run only what's needed for the iteration cadence.
|
||
/// P1: only "Core" data exists (Small/Medium/Large/Repeated/Deep). Comprehensive and Edge layers will be expanded in P2.
|
||
/// </summary>
|
||
private static List<TestDataSet> FilterByLayer(List<TestDataSet> all, string layer)
|
||
{
|
||
if (layer == "all") return all.ToList();
|
||
|
||
var coreNames = new[] { "Small", "Medium", "Large", "Repeated", "Deep" };
|
||
// P2 will add: "Flat", "Polymorphic", "Collection", "Numeric", "NonAscii", etc.
|
||
var comprehensiveExtras = new string[] { /* P2 */ };
|
||
// P3 will add: "ColdStart", "VeryLarge", "PathologicalString", etc.
|
||
var edgeExtras = new string[] { /* P3 */ };
|
||
|
||
bool StartsWithAny(string name, string[] prefixes) => prefixes.Any(p => name.StartsWith(p));
|
||
|
||
return layer switch
|
||
{
|
||
"core" => all.Where(t => StartsWithAny(t.Name, coreNames)).ToList(),
|
||
"comprehensive" => all.Where(t => StartsWithAny(t.Name, coreNames) || StartsWithAny(t.Name, comprehensiveExtras)).ToList(),
|
||
"edge" => all.Where(t => StartsWithAny(t.Name, coreNames) || StartsWithAny(t.Name, comprehensiveExtras) || StartsWithAny(t.Name, edgeExtras)).ToList(),
|
||
_ => all.ToList()
|
||
};
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Serializer Implementations
|
||
|
||
private interface ISerializerBenchmark
|
||
{
|
||
string Name { get; }
|
||
int SerializedSize { get; }
|
||
string? OptionsDescription => null;
|
||
void Warmup(int iterations);
|
||
void Serialize();
|
||
void Deserialize();
|
||
/// <summary>Round-trip correctness check — called once per cell before warmup. Returns true if Serialize+Deserialize preserves data.</summary>
|
||
bool VerifyRoundTrip();
|
||
}
|
||
|
||
private sealed class AcBinaryBenchmark : ISerializerBenchmark
|
||
{
|
||
private readonly TestOrder _order;
|
||
private readonly AcBinarySerializerOptions _options;
|
||
private readonly byte[] _serialized;
|
||
|
||
public string Name { get; }
|
||
public int SerializedSize => _serialized.Length;
|
||
public string OptionsDescription => $"WireMode={_options.WireMode}, RefHandling={_options.ReferenceHandling}, Interning={_options.UseStringInterning}, Metadata={_options.UseMetadata}, SGen={_options.UseGeneratedCode}, Compression={_options.UseCompression}";
|
||
|
||
public AcBinaryBenchmark(TestOrder order, AcBinarySerializerOptions options, string name)
|
||
{
|
||
_order = order;
|
||
_options = options;
|
||
Name = name;
|
||
_serialized = AcBinarySerializer.Serialize(order, options);
|
||
|
||
//_options.UseCompression = Lz4CompressionMode.Block;
|
||
}
|
||
|
||
public void Warmup(int iterations)
|
||
{
|
||
for (var i = 0; i < iterations; i++)
|
||
{
|
||
Serialize();
|
||
Deserialize();
|
||
}
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||
public void Serialize()
|
||
{
|
||
AcBinarySerializer.Serialize(_order, _options);
|
||
|
||
//if (_options.ReferenceHandling != ReferenceHandlingMode.None || _options.UseStringInterning != StringInterningMode.None)
|
||
//{
|
||
// AcBinarySerializer.ScanOnly(_order, _options);
|
||
//}
|
||
//else AcBinarySerializer.Serialize(_order, _options);
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||
public void Deserialize() => AcBinaryDeserializer.Deserialize<TestOrder>(_serialized, _options);
|
||
|
||
public bool VerifyRoundTrip()
|
||
{
|
||
var bytes = AcBinarySerializer.Serialize(_order, _options);
|
||
var roundTripped = AcBinaryDeserializer.Deserialize<TestOrder>(bytes, _options);
|
||
return DeepEqualsViaJson(_order, roundTripped);
|
||
}
|
||
}
|
||
|
||
private sealed class MemoryPackBenchmark : ISerializerBenchmark
|
||
{
|
||
private readonly TestOrder _order;
|
||
private readonly byte[] _serialized;
|
||
|
||
public string Name { get; }
|
||
public int SerializedSize => _serialized.Length;
|
||
|
||
public MemoryPackBenchmark(TestOrder order, string name)
|
||
{
|
||
_order = order;
|
||
Name = name;
|
||
_serialized = MemoryPackSerializer.Serialize(order);
|
||
}
|
||
|
||
public void Warmup(int iterations)
|
||
{
|
||
for (var i = 0; i < iterations; i++)
|
||
{
|
||
Serialize();
|
||
Deserialize();
|
||
}
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||
public void Serialize() => MemoryPackSerializer.Serialize(_order);
|
||
|
||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||
public void Deserialize() => MemoryPackSerializer.Deserialize<TestOrder>(_serialized);
|
||
|
||
public bool VerifyRoundTrip()
|
||
{
|
||
var bytes = MemoryPackSerializer.Serialize(_order);
|
||
var roundTripped = MemoryPackSerializer.Deserialize<TestOrder>(bytes);
|
||
return DeepEqualsViaJson(_order, roundTripped);
|
||
}
|
||
}
|
||
|
||
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 string OptionsDescription { get; }
|
||
|
||
public MessagePackBenchmark(TestOrder order, string name)
|
||
{
|
||
_order = order;
|
||
Name = name;
|
||
|
||
//_options = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
|
||
//_options = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.Lz4Block);
|
||
_options = MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.None);
|
||
|
||
var isContractless = _options.Resolver is ContractlessStandardResolver;
|
||
OptionsDescription = $"Mode={( isContractless ? "Contractless" : "ContractBased")}, Compression={_options.Compression}";
|
||
|
||
_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);
|
||
|
||
public bool VerifyRoundTrip()
|
||
{
|
||
var bytes = MessagePackSerializer.Serialize(_order, _options);
|
||
var roundTripped = MessagePackSerializer.Deserialize<TestOrder>(bytes, _options);
|
||
return DeepEqualsViaJson(_order, roundTripped);
|
||
}
|
||
}
|
||
|
||
private sealed class AcBinaryBufferWriterBenchmark : ISerializerBenchmark
|
||
{
|
||
private readonly TestOrder _order;
|
||
private readonly AcBinarySerializerOptions _options;
|
||
private readonly byte[] _serialized;
|
||
private ArrayBufferWriter<byte> _bufferWriter;
|
||
|
||
public string Name { get; }
|
||
public int SerializedSize => _serialized.Length;
|
||
public string OptionsDescription => $"WireMode={_options.WireMode}, RefHandling={_options.ReferenceHandling}, Interning={_options.UseStringInterning}, Metadata={_options.UseMetadata}, SGen={_options.UseGeneratedCode}, Compression={_options.UseCompression}";
|
||
|
||
public AcBinaryBufferWriterBenchmark(TestOrder order, AcBinarySerializerOptions options, string name)
|
||
{
|
||
_order = order;
|
||
_options = options;
|
||
Name = name;
|
||
_serialized = AcBinarySerializer.Serialize(order, options);
|
||
//_bufferWriter = new ArrayBufferWriter<byte>();
|
||
}
|
||
|
||
public void Warmup(int iterations)
|
||
{
|
||
for (var i = 0; i < iterations; i++)
|
||
{
|
||
Serialize();
|
||
Deserialize();
|
||
}
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||
public void Serialize()
|
||
{
|
||
//_bufferWriter.ResetWrittenCount();
|
||
_bufferWriter = new ArrayBufferWriter<byte>();
|
||
AcBinarySerializer.Serialize(_order, _bufferWriter, _options);
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||
public void Deserialize() => AcBinaryDeserializer.Deserialize<TestOrder>(_serialized, _options);
|
||
|
||
public bool VerifyRoundTrip()
|
||
{
|
||
var bw = new ArrayBufferWriter<byte>();
|
||
AcBinarySerializer.Serialize(_order, bw, _options);
|
||
var roundTripped = AcBinaryDeserializer.Deserialize<TestOrder>(bw.WrittenSpan.ToArray(), _options);
|
||
return DeepEqualsViaJson(_order, roundTripped);
|
||
}
|
||
}
|
||
|
||
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);
|
||
|
||
public bool VerifyRoundTrip()
|
||
{
|
||
var json = System.Text.Json.JsonSerializer.Serialize(_order, _options);
|
||
var roundTripped = System.Text.Json.JsonSerializer.Deserialize<TestOrder>(json, _options);
|
||
return DeepEqualsViaJson(_order, roundTripped);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Results
|
||
|
||
private sealed class BenchmarkResult
|
||
{
|
||
public string TestDataName { get; set; } = "";
|
||
public string SerializerName { get; set; } = "";
|
||
public string? OptionsDescription { get; set; }
|
||
public int SerializedSize { get; set; }
|
||
public double SerializeTimeMs { get; set; }
|
||
public double DeserializeTimeMs { get; set; }
|
||
public long SerializeAllocBytesPerOp { get; set; }
|
||
public long DeserializeAllocBytesPerOp { 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";
|
||
var serAlloc = result.SerializeTimeMs > 0 ? $"{result.SerializeAllocBytesPerOp,8:N0} B/op" : " N/A";
|
||
var desAlloc = result.DeserializeTimeMs > 0 ? $"{result.DeserializeAllocBytesPerOp,8:N0} B/op" : " N/A";
|
||
System.Console.WriteLine($" {result.SerializerName,-25} | Size: {result.SerializedSize,8:N0} | Ser: {ser} ({serAlloc}) | Des: {des} ({desAlloc})");
|
||
}
|
||
|
||
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("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
|
||
|
||
// Print serializer options
|
||
var optionsMap = results
|
||
.Where(r => r.OptionsDescription != null)
|
||
.Select(r => (r.SerializerName, r.OptionsDescription!))
|
||
.Distinct()
|
||
.ToList();
|
||
if (optionsMap.Count > 0)
|
||
{
|
||
System.Console.WriteLine();
|
||
System.Console.WriteLine(" Serializer Options:");
|
||
foreach (var (name, opts) in optionsMap)
|
||
System.Console.WriteLine($" {name}: {opts}");
|
||
}
|
||
|
||
foreach (var testData in testDataSets)
|
||
{
|
||
var testResults = results.Where(r => r.TestDataName == testData.DisplayName).OrderBy(r => r.RoundTripTimeMs).ToList();
|
||
// Baseline switched MessagePack → MemoryPack: MemoryPack is the SOTA performance leader.
|
||
var memPackResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerMemoryPack);
|
||
var acBinaryResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerAcBinaryDefault);
|
||
|
||
System.Console.WriteLine($"\n┌─ {testData.DisplayName} ─".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 MemoryPack (baseline) and AcBinary (Default) with win/lose colors
|
||
var isHighlighted = result.SerializerName is SerializerMemoryPack or SerializerAcBinaryDefault;
|
||
var prefix = isHighlighted ? "│►" : "│ ";
|
||
var suffix = isHighlighted ? "◄│" : " │";
|
||
|
||
// Color logic: Green = winner (faster), Red = loser (slower)
|
||
if (isHighlighted && memPackResult != null && acBinaryResult != null)
|
||
{
|
||
var isMemPack = result.SerializerName == SerializerMemoryPack;
|
||
var memPackFaster = memPackResult.RoundTripTimeMs < acBinaryResult.RoundTripTimeMs;
|
||
|
||
if (isMemPack)
|
||
{
|
||
System.Console.ForegroundColor = memPackFaster ? ConsoleColor.Green : ConsoleColor.Red;
|
||
}
|
||
else
|
||
{
|
||
System.Console.ForegroundColor = memPackFaster ? 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();
|
||
}
|
||
}
|
||
|
||
// Allocation summary row (per-op allocation in bytes; lower is better)
|
||
System.Console.WriteLine($"├{"─".PadRight(6, '─')}┼{"─".PadRight(27, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(14, '─')}┤");
|
||
foreach (var result in testResults)
|
||
{
|
||
if (result.SerializerName is not (SerializerMemoryPack or SerializerAcBinaryDefault)) continue;
|
||
var serAlloc = result.SerializeTimeMs > 0 ? $"{result.SerializeAllocBytesPerOp:N0} B/op" : "N/A";
|
||
var desAlloc = result.DeserializeTimeMs > 0 ? $"{result.DeserializeAllocBytesPerOp:N0} B/op" : "N/A";
|
||
System.Console.WriteLine($"│ alloc │ {result.SerializerName,-25} │ {"",10} │ {serAlloc,12} │ {desAlloc,12} │ {"",12} │");
|
||
}
|
||
|
||
// Footer row: AcBinary (Default) vs MemoryPack comparison per column
|
||
if (memPackResult != null && acBinaryResult != null)
|
||
{
|
||
var sizePct = (acBinaryResult.SerializedSize / (double)memPackResult.SerializedSize - 1) * 100;
|
||
var serPct = memPackResult.SerializeTimeMs > 0 ? (acBinaryResult.SerializeTimeMs / memPackResult.SerializeTimeMs - 1) * 100 : 0;
|
||
var desPct = memPackResult.DeserializeTimeMs > 0 ? (acBinaryResult.DeserializeTimeMs / memPackResult.DeserializeTimeMs - 1) * 100 : 0;
|
||
var rtPct = memPackResult.RoundTripTimeMs > 0 ? (acBinaryResult.RoundTripTimeMs / memPackResult.RoundTripTimeMs - 1) * 100 : 0;
|
||
|
||
System.Console.WriteLine($"├{"─".PadRight(6, '─')}┴{"─".PadRight(27, '─')}┼{"─".PadRight(12, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(14, '─')}┼{"─".PadRight(13, '─')}┤");
|
||
System.Console.Write($"│ ► Default vs {SerializerMemoryPack,-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, '─')}┘");
|
||
//System.Console.WriteLine($"GrowBufferCount: {AcBinarySerializer.GrowBufferCount}");
|
||
//System.Console.WriteLine($"GrowBufferTotalBytes: {AcBinarySerializer.GrowBufferTotalBytes:N0} bytes");
|
||
}
|
||
|
||
// 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 MemoryPack comparison (baseline switched MessagePack → MemoryPack as SOTA reference)
|
||
var memPackSerResults = results.Where(r => r.SerializerName == SerializerMemoryPack && r.SerializeTimeMs > 0).ToList();
|
||
var memPackDesResults = results.Where(r => r.SerializerName == SerializerMemoryPack && r.DeserializeTimeMs > 0).ToList();
|
||
var memPackRtResults = results.Where(r => r.SerializerName == SerializerMemoryPack && r.RoundTripTimeMs > 0).ToList();
|
||
|
||
var acBinarySerResults = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.SerializeTimeMs > 0).ToList();
|
||
var acBinaryDesResults = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.DeserializeTimeMs > 0).ToList();
|
||
var acBinaryRtResults = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.RoundTripTimeMs > 0).ToList();
|
||
|
||
// Skip comparison if no data available
|
||
if (memPackRtResults.Count == 0 || acBinaryRtResults.Count == 0)
|
||
{
|
||
System.Console.WriteLine();
|
||
System.Console.WriteLine($"── {SerializerAcBinaryDefault} vs {SerializerMemoryPack} (Overall) ──");
|
||
System.Console.WriteLine(" (Comparison requires both serialize and deserialize data)");
|
||
return;
|
||
}
|
||
|
||
var memPackAvgSer = memPackSerResults.Count > 0 ? memPackSerResults.Average(r => r.SerializeTimeMs) : 0;
|
||
var memPackAvgDes = memPackDesResults.Average(r => r.DeserializeTimeMs);
|
||
var memPackAvgRt = memPackRtResults.Average(r => r.RoundTripTimeMs);
|
||
var memPackAvgSize = results.Where(r => r.SerializerName == SerializerMemoryPack).Average(r => r.SerializedSize);
|
||
var memPackAvgSerAlloc = memPackSerResults.Count > 0 ? memPackSerResults.Average(r => r.SerializeAllocBytesPerOp) : 0;
|
||
var memPackAvgDesAlloc = memPackDesResults.Count > 0 ? memPackDesResults.Average(r => r.DeserializeAllocBytesPerOp) : 0;
|
||
|
||
var acBinaryAvgSer = acBinarySerResults.Count > 0 ? acBinarySerResults.Average(r => r.SerializeTimeMs) : 0;
|
||
var acBinaryAvgDes = acBinaryDesResults.Average(r => r.DeserializeTimeMs);
|
||
var acBinaryAvgRt = acBinaryRtResults.Average(r => r.RoundTripTimeMs);
|
||
var acBinaryAvgSize = results.Where(r => r.SerializerName == SerializerAcBinaryDefault).Average(r => r.SerializedSize);
|
||
var acBinaryAvgSerAlloc = acBinarySerResults.Count > 0 ? acBinarySerResults.Average(r => r.SerializeAllocBytesPerOp) : 0;
|
||
var acBinaryAvgDesAlloc = acBinaryDesResults.Count > 0 ? acBinaryDesResults.Average(r => r.DeserializeAllocBytesPerOp) : 0;
|
||
|
||
System.Console.WriteLine();
|
||
System.Console.WriteLine($"── {SerializerAcBinaryDefault} vs {SerializerMemoryPack} (Overall) ──");
|
||
|
||
// Only show serialize comparison if data available
|
||
if (memPackAvgSer > 0 && acBinaryAvgSer > 0)
|
||
{
|
||
var serPctAll = (acBinaryAvgSer / memPackAvgSer - 1) * 100;
|
||
System.Console.ForegroundColor = serPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||
System.Console.WriteLine($" Serialize: {serPctAll:+0;-0}% ({acBinaryAvgSer:F2} ms vs {memPackAvgSer:F2} ms)");
|
||
System.Console.ResetColor();
|
||
}
|
||
|
||
var desPctAll = (acBinaryAvgDes / memPackAvgDes - 1) * 100;
|
||
var rtPctAll = (acBinaryAvgRt / memPackAvgRt - 1) * 100;
|
||
var sizePctAll = (acBinaryAvgSize / memPackAvgSize - 1) * 100;
|
||
|
||
System.Console.ForegroundColor = desPctAll <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||
System.Console.WriteLine($" Deserialize: {desPctAll:+0;-0}% ({acBinaryAvgDes:F2} ms vs {memPackAvgDes: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 {memPackAvgRt: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 {memPackAvgSize:F0} B)");
|
||
System.Console.ResetColor();
|
||
|
||
// Allocation comparison: byte[] API allocates the output array on both sides — delta shows serializer-overhead diff.
|
||
if (memPackAvgSerAlloc > 0 && acBinaryAvgSerAlloc > 0)
|
||
{
|
||
var serAllocPct = (acBinaryAvgSerAlloc / memPackAvgSerAlloc - 1) * 100;
|
||
System.Console.ForegroundColor = serAllocPct <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||
System.Console.WriteLine($" Ser Alloc: {serAllocPct:+0;-0}% ({acBinaryAvgSerAlloc:F0} B/op vs {memPackAvgSerAlloc:F0} B/op)");
|
||
System.Console.ResetColor();
|
||
}
|
||
if (memPackAvgDesAlloc > 0 && acBinaryAvgDesAlloc > 0)
|
||
{
|
||
var desAllocPct = (acBinaryAvgDesAlloc / memPackAvgDesAlloc - 1) * 100;
|
||
System.Console.ForegroundColor = desAllocPct <= 0 ? ConsoleColor.Green : ConsoleColor.Red;
|
||
System.Console.WriteLine($" Des Alloc: {desAllocPct:+0;-0}% ({acBinaryAvgDesAlloc:F0} B/op vs {memPackAvgDesAlloc:F0} B/op)");
|
||
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 baseFileName = $"Console.FullBenchmark_{BuildConfiguration}_{timestamp}";
|
||
var logFilePath = Path.Combine(ResultsDirectory, $"{baseFileName}.log");
|
||
var outputFilePath = Path.Combine(ResultsDirectory, $"{baseFileName}.output");
|
||
|
||
// Save binary output to separate .output file
|
||
var largeTestData = testDataSets.FirstOrDefault(t => t.Name.StartsWith("Large"));
|
||
if (largeTestData != null)
|
||
{
|
||
var outputSb = new StringBuilder();
|
||
outputSb.AppendLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗");
|
||
outputSb.AppendLine("║ SERIALIZED BINARY OUTPUT ║");
|
||
outputSb.AppendLine($"║ Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}".PadRight(100) + "║");
|
||
outputSb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
|
||
outputSb.AppendLine();
|
||
|
||
outputSb.AppendLine("=== SERIALIZED BYTES: Large (5x5x5x10) - AcBinary (Default) ===");
|
||
var serializedBytes = AcBinarySerializer.Serialize(largeTestData.Order, AcBinarySerializerOptions.Default);
|
||
outputSb.AppendLine($"Size: {serializedBytes.Length:N0} bytes");
|
||
outputSb.AppendLine();
|
||
outputSb.AppendLine("Hex dump:");
|
||
outputSb.AppendLine(FormatHexDump(serializedBytes));
|
||
|
||
File.WriteAllText(outputFilePath, outputSb.ToString(), Utf8NoBom);
|
||
System.Console.WriteLine($"✓ Binary output saved to: {outputFilePath}");
|
||
}
|
||
|
||
// Save benchmark results to .log file
|
||
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($"║ Samples: {BenchmarkSamples} (median)".PadRight(100) + "║");
|
||
sb.AppendLine($"║ Test Type: {testDataSets.FirstOrDefault()?.TypeName ?? "unknown"}".PadRight(100) + "║");
|
||
sb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
|
||
sb.AppendLine();
|
||
|
||
// Serializer options summary
|
||
var optionsMap = results
|
||
.Where(r => r.OptionsDescription != null)
|
||
.Select(r => (r.SerializerName, r.OptionsDescription!))
|
||
.Distinct()
|
||
.ToList();
|
||
if (optionsMap.Count > 0)
|
||
{
|
||
sb.AppendLine("=== SERIALIZER OPTIONS ===");
|
||
foreach (var (name, opts) in optionsMap)
|
||
sb.AppendLine($" {name}: {opts}");
|
||
sb.AppendLine();
|
||
}
|
||
|
||
// CSV-like data for easy import (now includes per-op allocation columns)
|
||
sb.AppendLine("=== RAW DATA (CSV) ===");
|
||
sb.AppendLine("TestData,Serializer,Size,SerializeMs,DeserializeMs,RoundTripMs,SerializeAllocBytesPerOp,DeserializeAllocBytesPerOp");
|
||
foreach (var testData in testDataSets)
|
||
{
|
||
var testResults = results.Where(r => r.TestDataName == testData.DisplayName).ToList();
|
||
foreach (var result in testResults)
|
||
{
|
||
sb.AppendLine($"{result.TestDataName},{result.SerializerName},{result.SerializedSize},{result.SerializeTimeMs:F2},{result.DeserializeTimeMs:F2},{result.RoundTripTimeMs:F2},{result.SerializeAllocBytesPerOp},{result.DeserializeAllocBytesPerOp}");
|
||
}
|
||
}
|
||
sb.AppendLine();
|
||
|
||
// Formatted results
|
||
sb.AppendLine("=== FORMATTED RESULTS BY TEST DATA ===");
|
||
sb.AppendLine($"(►) = Highlighted: {SerializerMemoryPack} (baseline) and {SerializerAcBinaryDefault}");
|
||
sb.AppendLine();
|
||
|
||
foreach (var testData in testDataSets)
|
||
{
|
||
var testResults = results.Where(r => r.TestDataName == testData.DisplayName).OrderBy(r => r.RoundTripTimeMs).ToList();
|
||
var memPackResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerMemoryPack);
|
||
var acBinaryResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerAcBinaryDefault);
|
||
|
||
sb.AppendLine();
|
||
sb.AppendLine($"--- {testData.DisplayName} ---");
|
||
sb.AppendLine($"{"#",-4} {"Serializer",-26} {"Size",-12} {"Serialize",-14} {"Deserialize",-14} {"Round-trip",-14} {"SerAlloc",-12} {"DesAlloc",-12}");
|
||
sb.AppendLine(new string('-', 110));
|
||
|
||
var rank = 1;
|
||
foreach (var result in testResults)
|
||
{
|
||
var isHighlighted = result.SerializerName is SerializerMemoryPack 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";
|
||
var serAlloc = result.SerializeTimeMs > 0 ? $"{result.SerializeAllocBytesPerOp:N0} B" : "N/A";
|
||
var desAlloc = result.DeserializeTimeMs > 0 ? $"{result.DeserializeAllocBytesPerOp:N0} B" : "N/A";
|
||
|
||
sb.AppendLine($"{rank++,2} {prefix}{result.SerializerName,-24} {size,-12} {ser,-14} {des,-14} {rt,-14} {serAlloc,-12} {desAlloc,-12}");
|
||
}
|
||
|
||
// Summary row for this test data (vs MemoryPack — baseline switched MessagePack → MemoryPack)
|
||
if (memPackResult != null && acBinaryResult != null)
|
||
{
|
||
var sizePct = (acBinaryResult.SerializedSize / (double)memPackResult.SerializedSize - 1) * 100;
|
||
var serPct = memPackResult.SerializeTimeMs > 0 ? (acBinaryResult.SerializeTimeMs / memPackResult.SerializeTimeMs - 1) * 100 : 0;
|
||
var desPct = memPackResult.DeserializeTimeMs > 0 ? (acBinaryResult.DeserializeTimeMs / memPackResult.DeserializeTimeMs - 1) * 100 : 0;
|
||
var rtPct = memPackResult.RoundTripTimeMs > 0 ? (acBinaryResult.RoundTripTimeMs / memPackResult.RoundTripTimeMs - 1) * 100 : 0;
|
||
|
||
sb.AppendLine($" {SerializerAcBinaryDefault} vs {SerializerMemoryPack}: Size {sizePct:+0;-0}% │ Ser {serPct:+0;-0}% │ Des {desPct:+0;-0}% │ RT {rtPct:+0;-0}%");
|
||
}
|
||
|
||
//sb.AppendLine($"GrowBufferCount: {AcBinarySerializer.GrowBufferCount}");
|
||
//sb.AppendLine($"GrowBufferTotalBytes: {AcBinarySerializer.GrowBufferTotalBytes:N0} bytes");
|
||
}
|
||
|
||
|
||
// Summary comparison (vs MemoryPack)
|
||
sb.AppendLine();
|
||
sb.AppendLine($"=== {SerializerAcBinaryDefault} vs {SerializerMemoryPack} (Overall) ===");
|
||
|
||
var memPackSerResults2 = results.Where(r => r.SerializerName == SerializerMemoryPack && r.SerializeTimeMs > 0).ToList();
|
||
var memPackDesResults2 = results.Where(r => r.SerializerName == SerializerMemoryPack && r.DeserializeTimeMs > 0).ToList();
|
||
var memPackRtResults2 = results.Where(r => r.SerializerName == SerializerMemoryPack && r.RoundTripTimeMs > 0).ToList();
|
||
|
||
var acBinarySerResults2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.SerializeTimeMs > 0).ToList();
|
||
var acBinaryDesResults2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.DeserializeTimeMs > 0).ToList();
|
||
var acBinaryRtResults2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault && r.RoundTripTimeMs > 0).ToList();
|
||
|
||
if (memPackSerResults2.Count > 0 && acBinarySerResults2.Count > 0)
|
||
{
|
||
var memPackAvgSer2 = memPackSerResults2.Average(r => r.SerializeTimeMs);
|
||
var acBinaryAvgSer2 = acBinarySerResults2.Average(r => r.SerializeTimeMs);
|
||
var memPackAvgSerAlloc2 = memPackSerResults2.Average(r => r.SerializeAllocBytesPerOp);
|
||
var acBinaryAvgSerAlloc2 = acBinarySerResults2.Average(r => r.SerializeAllocBytesPerOp);
|
||
sb.AppendLine($" Serialize: {((acBinaryAvgSer2 / memPackAvgSer2 - 1) * 100):+0;-0}% ({acBinaryAvgSer2:F2} ms vs {memPackAvgSer2:F2} ms)");
|
||
if (memPackAvgSerAlloc2 > 0)
|
||
sb.AppendLine($" Ser Alloc: {((acBinaryAvgSerAlloc2 / memPackAvgSerAlloc2 - 1) * 100):+0;-0}% ({acBinaryAvgSerAlloc2:F0} B/op vs {memPackAvgSerAlloc2:F0} B/op)");
|
||
}
|
||
|
||
if (memPackDesResults2.Count > 0 && acBinaryDesResults2.Count > 0)
|
||
{
|
||
var memPackAvgDes2 = memPackDesResults2.Average(r => r.DeserializeTimeMs);
|
||
var acBinaryAvgDes2 = acBinaryDesResults2.Average(r => r.DeserializeTimeMs);
|
||
var memPackAvgDesAlloc2 = memPackDesResults2.Average(r => r.DeserializeAllocBytesPerOp);
|
||
var acBinaryAvgDesAlloc2 = acBinaryDesResults2.Average(r => r.DeserializeAllocBytesPerOp);
|
||
sb.AppendLine($" Deserialize: {((acBinaryAvgDes2 / memPackAvgDes2 - 1) * 100):+0;-0}% ({acBinaryAvgDes2:F2} ms vs {memPackAvgDes2:F2} ms)");
|
||
if (memPackAvgDesAlloc2 > 0)
|
||
sb.AppendLine($" Des Alloc: {((acBinaryAvgDesAlloc2 / memPackAvgDesAlloc2 - 1) * 100):+0;-0}% ({acBinaryAvgDesAlloc2:F0} B/op vs {memPackAvgDesAlloc2:F0} B/op)");
|
||
}
|
||
|
||
if (memPackRtResults2.Count > 0 && acBinaryRtResults2.Count > 0)
|
||
{
|
||
var memPackAvgRt2 = memPackRtResults2.Average(r => r.RoundTripTimeMs);
|
||
var acBinaryAvgRt2 = acBinaryRtResults2.Average(r => r.RoundTripTimeMs);
|
||
sb.AppendLine($" Round-trip: {((acBinaryAvgRt2 / memPackAvgRt2 - 1) * 100):+0;-0}% ({acBinaryAvgRt2:F2} ms vs {memPackAvgRt2:F2} ms)");
|
||
}
|
||
|
||
var memPackAvgSize2 = results.Where(r => r.SerializerName == SerializerMemoryPack).Average(r => r.SerializedSize);
|
||
var acBinaryAvgSize2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault).Average(r => r.SerializedSize);
|
||
sb.AppendLine($" Size: {((acBinaryAvgSize2 / memPackAvgSize2 - 1) * 100):+0;-0}% ({acBinaryAvgSize2:F0} B vs {memPackAvgSize2:F0} B)");
|
||
|
||
File.WriteAllText(logFilePath, sb.ToString(), Utf8NoBom);
|
||
System.Console.WriteLine($"✓ Results saved to: {logFilePath}");
|
||
|
||
// Save LLM-optimized results
|
||
var llmFilePath = Path.Combine(ResultsDirectory, $"{baseFileName}.LLM");
|
||
SaveLlmResults(llmFilePath, results, testDataSets);
|
||
}
|
||
|
||
private static void SaveLlmResults(string filePath, List<BenchmarkResult> results, List<TestDataSet> testDataSets)
|
||
{
|
||
var sb = new StringBuilder();
|
||
var testTypeName = testDataSets.FirstOrDefault()?.TypeName ?? "unknown";
|
||
sb.AppendLine($"# AcBinary Benchmark {BuildConfiguration} {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
|
||
sb.AppendLine($"Iterations: {TestIterations} | Warmup: {WarmupIterations} | Samples: {BenchmarkSamples} (median) | .NET: {Environment.Version} | TestType: {testTypeName}");
|
||
sb.AppendLine($"Baseline: {SerializerMemoryPack} (SOTA reference) | Verified: round-trip correctness checked once per cell before warmup");
|
||
|
||
// Options summary
|
||
var optionsMap = results
|
||
.Where(r => r.OptionsDescription != null)
|
||
.Select(r => (r.SerializerName, r.OptionsDescription!))
|
||
.Distinct()
|
||
.ToList();
|
||
if (optionsMap.Count > 0)
|
||
{
|
||
sb.AppendLine();
|
||
sb.AppendLine("## Options");
|
||
sb.AppendLine();
|
||
foreach (var (name, opts) in optionsMap)
|
||
sb.AppendLine($"- **{name}**: {opts}");
|
||
}
|
||
|
||
// Flat results table sorted by test data then round-trip (now includes Alloc columns)
|
||
sb.AppendLine();
|
||
sb.AppendLine("## Results");
|
||
sb.AppendLine();
|
||
sb.AppendLine("TestData | Serializer | Size(B) | Ser(ms) | Deser(ms) | RT(ms) | SerAlloc(B/op) | DesAlloc(B/op)");
|
||
sb.AppendLine("---|---|---|---|---|---|---|---");
|
||
|
||
foreach (var testData in testDataSets)
|
||
{
|
||
var testResults = results
|
||
.Where(r => r.TestDataName == testData.DisplayName)
|
||
.OrderBy(r => r.RoundTripTimeMs)
|
||
.ToList();
|
||
|
||
foreach (var r in testResults)
|
||
{
|
||
var inv = System.Globalization.CultureInfo.InvariantCulture;
|
||
var ser = r.SerializeTimeMs > 0 ? r.SerializeTimeMs.ToString("F2", inv) : "-";
|
||
var des = r.DeserializeTimeMs > 0 ? r.DeserializeTimeMs.ToString("F2", inv) : "-";
|
||
var rt = r.RoundTripTimeMs > 0 ? r.RoundTripTimeMs.ToString("F2", inv) : "-";
|
||
var serAlloc = r.SerializeTimeMs > 0 ? r.SerializeAllocBytesPerOp.ToString(inv) : "-";
|
||
var desAlloc = r.DeserializeTimeMs > 0 ? r.DeserializeAllocBytesPerOp.ToString(inv) : "-";
|
||
sb.AppendLine($"{r.TestDataName} | {r.SerializerName} | {r.SerializedSize} | {ser} | {des} | {rt} | {serAlloc} | {desAlloc}");
|
||
}
|
||
}
|
||
|
||
|
||
File.WriteAllText(filePath, sb.ToString(), Utf8NoBom);
|
||
System.Console.WriteLine($"✓ LLM results saved to: {filePath}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Formats byte array as hex dump with offset, hex values, and ASCII representation.
|
||
/// </summary>
|
||
private static string FormatHexDump(byte[] bytes, int bytesPerLine = 16)
|
||
{
|
||
var sb = new StringBuilder();
|
||
for (var i = 0; i < bytes.Length; i += bytesPerLine)
|
||
{
|
||
// Offset
|
||
sb.Append($"{i:X8} ");
|
||
|
||
// Hex bytes
|
||
for (var j = 0; j < bytesPerLine; j++)
|
||
{
|
||
if (i + j < bytes.Length)
|
||
sb.Append($"{bytes[i + j]:X2} ");
|
||
else
|
||
sb.Append(" ");
|
||
|
||
if (j == 7) sb.Append(' '); // Extra space in middle
|
||
}
|
||
|
||
sb.Append(" |");
|
||
|
||
// ASCII representation
|
||
for (var j = 0; j < bytesPerLine && i + j < bytes.Length; j++)
|
||
{
|
||
var b = bytes[i + j];
|
||
sb.Append(b is >= 32 and < 127 ? (char)b : '.');
|
||
}
|
||
|
||
sb.AppendLine("|");
|
||
}
|
||
return sb.ToString();
|
||
}
|
||
|
||
#endregion
|
||
}
|