843 lines
41 KiB
C#
843 lines
41 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 SerializerAcBinaryNoRef = "AcBinary (NoRef)";
|
|
private const string SerializerAcBinaryFastMode = "AcBinary (FastMode)";
|
|
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 = 5;
|
|
private static int TestIterations = 10;
|
|
#else
|
|
private static int WarmupIterations = 2000;
|
|
private static int TestIterations = 1000;
|
|
|
|
//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;
|
|
|
|
var mode = args.Length > 0 ? args[0].ToLower() : "all";
|
|
|
|
if (mode == "quick")
|
|
{
|
|
WarmupIterations = 5;
|
|
TestIterations = 100;
|
|
mode = "all";
|
|
}
|
|
|
|
// Profiler mode: warmup only, then exit (for memory profiler analysis)
|
|
if (mode == "profiler")
|
|
{
|
|
RunProfilerMode();
|
|
return;
|
|
}
|
|
|
|
System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════╗");
|
|
System.Console.WriteLine("║ COMPREHENSIVE SERIALIZER BENCHMARK SUITE ║");
|
|
System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════╝");
|
|
System.Console.WriteLine($"Mode: {mode} | Iterations: {TestIterations} | Warmup: {WarmupIterations}");
|
|
System.Console.WriteLine($"Build: {BuildConfiguration} | .NET: {Environment.Version}");
|
|
System.Console.WriteLine();
|
|
|
|
var allResults = new List<BenchmarkResult>();
|
|
var testDataSets = BenchmarkTestDataProvider.CreateTestDataSets();
|
|
|
|
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, mode);
|
|
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);
|
|
|
|
// 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(2000);
|
|
|
|
// Run benchmarks
|
|
System.Console.WriteLine($"Running benchmarks ({TestIterations} iterations)...\n");
|
|
|
|
foreach (var serializer in serializers)
|
|
{
|
|
var result = new BenchmarkResult
|
|
{
|
|
TestDataName = testData.DisplayName, // Use DisplayName for IId% info
|
|
SerializerName = serializer.Name,
|
|
SerializedSize = serializer.SerializedSize
|
|
};
|
|
|
|
if (mode is "all" or "serialize" or "ser")
|
|
{
|
|
result.SerializeTimeMs = RunTimed(() => serializer.Serialize(), TestIterations);
|
|
}
|
|
|
|
if (mode is "all" or "deserialize" or "des")
|
|
{
|
|
result.DeserializeTimeMs = RunTimed(() => serializer.Deserialize(), TestIterations);
|
|
}
|
|
|
|
results.Add(result);
|
|
PrintResult(result);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private static List<ISerializerBenchmark> CreateSerializers(TestDataSet testData)
|
|
{
|
|
return new List<ISerializerBenchmark>
|
|
{
|
|
|
|
// AcBinary variants
|
|
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
|
|
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef),
|
|
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
|
|
new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None }, SerializerAcBinaryNoIntern),
|
|
|
|
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefault),
|
|
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoRef),
|
|
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
|
|
//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)
|
|
};
|
|
}
|
|
|
|
private static double RunTimed(Action action, int iterations)
|
|
{
|
|
var sw = Stopwatch.StartNew();
|
|
for (var i = 0; i < iterations; i++)
|
|
{
|
|
action();
|
|
}
|
|
sw.Stop();
|
|
return sw.Elapsed.TotalMilliseconds;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Serializer Implementations
|
|
|
|
private interface ISerializerBenchmark
|
|
{
|
|
string Name { get; }
|
|
int SerializedSize { get; }
|
|
void Warmup(int iterations);
|
|
void Serialize();
|
|
void Deserialize();
|
|
}
|
|
|
|
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 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);
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
public void Deserialize() => AcBinaryDeserializer.Deserialize<TestOrder>(_serialized, _options);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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);
|
|
//_options = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.Lz4Block);
|
|
_options = MessagePackSerializerOptions.Standard.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 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 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);
|
|
}
|
|
|
|
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 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.DisplayName).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.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 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, '─')}┘");
|
|
//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 MessagePack comparison
|
|
var msgPackSerResults = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).ToList();
|
|
var msgPackDesResults = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).ToList();
|
|
var msgPackRtResults = results.Where(r => r.SerializerName == SerializerMessagePack && 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 (msgPackRtResults.Count == 0 || acBinaryRtResults.Count == 0)
|
|
{
|
|
System.Console.WriteLine();
|
|
System.Console.WriteLine($"── {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ──");
|
|
System.Console.WriteLine(" (Comparison requires both serialize and deserialize data)");
|
|
return;
|
|
}
|
|
|
|
var msgPackAvgSer = msgPackSerResults.Count > 0 ? msgPackSerResults.Average(r => r.SerializeTimeMs) : 0;
|
|
var msgPackAvgDes = msgPackDesResults.Average(r => r.DeserializeTimeMs);
|
|
var msgPackAvgRt = msgPackRtResults.Average(r => r.RoundTripTimeMs);
|
|
var msgPackAvgSize = results.Where(r => r.SerializerName == SerializerMessagePack).Average(r => r.SerializedSize);
|
|
|
|
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);
|
|
|
|
System.Console.WriteLine();
|
|
System.Console.WriteLine($"── {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ──");
|
|
|
|
// Only show serialize comparison if data available
|
|
if (msgPackAvgSer > 0 && acBinaryAvgSer > 0)
|
|
{
|
|
var serPctAll = (acBinaryAvgSer / msgPackAvgSer - 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();
|
|
}
|
|
|
|
var desPctAll = (acBinaryAvgDes / msgPackAvgDes - 1) * 100;
|
|
var rtPctAll = (acBinaryAvgRt / msgPackAvgRt - 1) * 100;
|
|
var sizePctAll = (acBinaryAvgSize / msgPackAvgSize - 1) * 100;
|
|
|
|
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 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("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
|
|
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.DisplayName).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.DisplayName).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.DisplayName} ---");
|
|
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}%");
|
|
}
|
|
|
|
//sb.AppendLine($"GrowBufferCount: {AcBinarySerializer.GrowBufferCount}");
|
|
//sb.AppendLine($"GrowBufferTotalBytes: {AcBinarySerializer.GrowBufferTotalBytes:N0} bytes");
|
|
}
|
|
|
|
|
|
// Summary comparison
|
|
sb.AppendLine();
|
|
sb.AppendLine($"=== {SerializerAcBinaryDefault} vs {SerializerMessagePack} (Overall) ===");
|
|
|
|
var msgPackSerResults2 = results.Where(r => r.SerializerName == SerializerMessagePack && r.SerializeTimeMs > 0).ToList();
|
|
var msgPackDesResults2 = results.Where(r => r.SerializerName == SerializerMessagePack && r.DeserializeTimeMs > 0).ToList();
|
|
var msgPackRtResults2 = results.Where(r => r.SerializerName == SerializerMessagePack && 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 (msgPackSerResults2.Count > 0 && acBinarySerResults2.Count > 0)
|
|
{
|
|
var msgPackAvgSer2 = msgPackSerResults2.Average(r => r.SerializeTimeMs);
|
|
var acBinaryAvgSer2 = acBinarySerResults2.Average(r => r.SerializeTimeMs);
|
|
sb.AppendLine($" Serialize: {((acBinaryAvgSer2 / msgPackAvgSer2 - 1) * 100):+0;-0}% ({acBinaryAvgSer2:F2} ms vs {msgPackAvgSer2:F2} ms)");
|
|
}
|
|
|
|
if (msgPackDesResults2.Count > 0 && acBinaryDesResults2.Count > 0)
|
|
{
|
|
var msgPackAvgDes2 = msgPackDesResults2.Average(r => r.DeserializeTimeMs);
|
|
var acBinaryAvgDes2 = acBinaryDesResults2.Average(r => r.DeserializeTimeMs);
|
|
sb.AppendLine($" Deserialize: {((acBinaryAvgDes2 / msgPackAvgDes2 - 1) * 100):+0;-0}% ({acBinaryAvgDes2:F2} ms vs {msgPackAvgDes2:F2} ms)");
|
|
}
|
|
|
|
if (msgPackRtResults2.Count > 0 && acBinaryRtResults2.Count > 0)
|
|
{
|
|
var msgPackAvgRt2 = msgPackRtResults2.Average(r => r.RoundTripTimeMs);
|
|
var acBinaryAvgRt2 = acBinaryRtResults2.Average(r => r.RoundTripTimeMs);
|
|
sb.AppendLine($" Round-trip: {((acBinaryAvgRt2 / msgPackAvgRt2 - 1) * 100):+0;-0}% ({acBinaryAvgRt2:F2} ms vs {msgPackAvgRt2:F2} ms)");
|
|
}
|
|
|
|
var msgPackAvgSize2 = results.Where(r => r.SerializerName == SerializerMessagePack).Average(r => r.SerializedSize);
|
|
var acBinaryAvgSize2 = results.Where(r => r.SerializerName == SerializerAcBinaryDefault).Average(r => r.SerializedSize);
|
|
sb.AppendLine($" Size: {((acBinaryAvgSize2 / msgPackAvgSize2 - 1) * 100):+0;-0}% ({acBinaryAvgSize2:F0} B vs {msgPackAvgSize2:F0} B)");
|
|
|
|
File.WriteAllText(logFilePath, sb.ToString(), Utf8NoBom);
|
|
System.Console.WriteLine($"✓ Results saved to: {logFilePath}");
|
|
}
|
|
|
|
/// <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
|
|
}
|