950 lines
42 KiB
C#
950 lines
42 KiB
C#
using System.Diagnostics;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using AyCode.Core.Serializers.Binaries;
|
|
using AyCode.Core.Serializers.Jsons;
|
|
using AyCode.Core.Tests.TestModels;
|
|
using MessagePack;
|
|
using MessagePack.Resolvers;
|
|
using Newtonsoft.Json;
|
|
|
|
namespace AyCode.Core.Serializers.Console;
|
|
|
|
/// <summary>
|
|
/// Comprehensive benchmark application for all serializers.
|
|
/// Compares: AcBinary (all options), AcJson, 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 SerializerAcJsonDefault = "AcJson (Default)";
|
|
private const string SerializerNewtonsoftJson = "Newtonsoft.Json";
|
|
private const string SerializerSystemTextJson = "System.Text.Json";
|
|
|
|
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
|
|
|
|
private static int WarmupIterations = 10;
|
|
private static int TestIterations = 1000;
|
|
|
|
public static void Main(string[] args)
|
|
{
|
|
// Set console encoding to UTF-8 for proper Unicode character display
|
|
System.Console.OutputEncoding = System.Text.Encoding.UTF8;
|
|
|
|
var mode = args.Length > 0 ? args[0].ToLower() : "all";
|
|
|
|
if (mode == "quick")
|
|
{
|
|
WarmupIterations = 10;
|
|
TestIterations = 100;
|
|
mode = "all";
|
|
}
|
|
|
|
System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════╗");
|
|
System.Console.WriteLine("║ COMPREHENSIVE SERIALIZER BENCHMARK SUITE ║");
|
|
System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════╝");
|
|
System.Console.WriteLine($"Mode: {mode} | Iterations: {TestIterations} | Warmup: {WarmupIterations}");
|
|
System.Console.WriteLine($"Build: {BuildConfiguration} | .NET: {Environment.Version}");
|
|
System.Console.WriteLine();
|
|
|
|
var allResults = new List<BenchmarkResult>();
|
|
var testDataSets = CreateTestDataSets();
|
|
|
|
foreach (var testData in testDataSets)
|
|
{
|
|
System.Console.WriteLine($"\n{'═'.ToString().PadRight(70, '═')}");
|
|
System.Console.WriteLine($"TEST DATA: {testData.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!");
|
|
}
|
|
|
|
#region Test Data Creation
|
|
|
|
private static List<TestDataSet> CreateTestDataSets()
|
|
{
|
|
return new List<TestDataSet>
|
|
{
|
|
CreateSmallTestData(),
|
|
CreateMediumTestData(),
|
|
CreateLargeTestData(),
|
|
CreateRepeatedStringsTestData(),
|
|
CreateDeepNestedTestData()
|
|
};
|
|
}
|
|
|
|
private static TestDataSet CreateSmallTestData()
|
|
{
|
|
TestDataFactory.ResetIdCounter();
|
|
|
|
// Create shared references - IId types (only at Order/Item level)
|
|
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
|
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
|
|
|
var order = TestDataFactory.CreateOrder(
|
|
itemCount: 2,
|
|
palletsPerItem: 2,
|
|
measurementsPerPallet: 2,
|
|
pointsPerMeasurement: 2,
|
|
sharedTag: sharedTag,
|
|
sharedUser: sharedUser);
|
|
|
|
// Clear deeper level refs for realistic ~10% ratio
|
|
ClearDeepLevelRefs(order);
|
|
|
|
return new TestDataSet("Small (2x2x2x2)", order, iidRefPercent: 10);
|
|
}
|
|
|
|
private static TestDataSet CreateMediumTestData()
|
|
{
|
|
TestDataFactory.ResetIdCounter();
|
|
|
|
// IId shared references
|
|
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
|
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
|
var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);
|
|
|
|
// Non-IId shared reference - create separate preferences for 2 users
|
|
var sharedPreferences = new UserPreferences
|
|
{
|
|
Theme = "dark",
|
|
Language = "en-US",
|
|
NotificationsEnabled = true,
|
|
EmailDigestFrequency = "weekly"
|
|
};
|
|
sharedUser.Preferences = sharedPreferences;
|
|
|
|
var order = TestDataFactory.CreateOrder(
|
|
itemCount: 3,
|
|
palletsPerItem: 3,
|
|
measurementsPerPallet: 3,
|
|
pointsPerMeasurement: 4,
|
|
sharedTag: sharedTag,
|
|
sharedUser: sharedUser,
|
|
sharedMetadata: sharedMeta,
|
|
sharedPreferences: sharedPreferences);
|
|
|
|
// Clear deeper level refs for realistic ~10% ratio
|
|
ClearDeepLevelRefs(order);
|
|
|
|
return new TestDataSet("Medium (3x3x3x4)", order, iidRefPercent: 10);
|
|
}
|
|
|
|
private static TestDataSet CreateLargeTestData()
|
|
{
|
|
TestDataFactory.ResetIdCounter();
|
|
|
|
// IId shared references
|
|
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
|
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
|
|
|
// Non-IId shared reference
|
|
var sharedPreferences = new UserPreferences
|
|
{
|
|
Theme = "light",
|
|
Language = "de-DE",
|
|
NotificationsEnabled = false,
|
|
EmailDigestFrequency = "daily"
|
|
};
|
|
sharedUser.Preferences = sharedPreferences;
|
|
|
|
var order = TestDataFactory.CreateOrder(
|
|
itemCount: 5,
|
|
palletsPerItem: 5,
|
|
measurementsPerPallet: 5,
|
|
pointsPerMeasurement: 10,
|
|
sharedTag: sharedTag,
|
|
sharedUser: sharedUser,
|
|
sharedPreferences: sharedPreferences);
|
|
|
|
// Clear deeper level refs for realistic ~10% ratio
|
|
ClearDeepLevelRefs(order);
|
|
|
|
return new TestDataSet("Large (5x5x5x10)", order, iidRefPercent: 10);
|
|
}
|
|
|
|
private static TestDataSet CreateRepeatedStringsTestData()
|
|
{
|
|
TestDataFactory.ResetIdCounter();
|
|
|
|
// IId shared references
|
|
var sharedTag = TestDataFactory.CreateTag("RepeatedTag");
|
|
var sharedUser = TestDataFactory.CreateUser("repeateduser");
|
|
|
|
// Non-IId shared reference
|
|
var sharedPreferences = new UserPreferences
|
|
{
|
|
Theme = "dark",
|
|
Language = "en-US",
|
|
NotificationsEnabled = true,
|
|
EmailDigestFrequency = "weekly"
|
|
};
|
|
sharedUser.Preferences = sharedPreferences;
|
|
|
|
// 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,
|
|
sharedTag: sharedTag,
|
|
sharedUser: sharedUser,
|
|
sharedPreferences: sharedPreferences);
|
|
|
|
// 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";
|
|
}
|
|
|
|
// Clear deeper level refs for realistic ~10% ratio
|
|
ClearDeepLevelRefs(order);
|
|
|
|
return new TestDataSet("Repeated Strings (10 items)", order, iidRefPercent: 10);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears IId shared references from Pallet, Measurement, and Point levels.
|
|
/// This creates a realistic ~10% IId ref ratio (only Order and Item levels have refs).
|
|
/// </summary>
|
|
private static void ClearDeepLevelRefs(TestOrder order)
|
|
{
|
|
foreach (var item in order.Items)
|
|
{
|
|
foreach (var pallet in item.Pallets)
|
|
{
|
|
pallet.Tag = null;
|
|
pallet.Inspector = null;
|
|
pallet.Category = null;
|
|
|
|
foreach (var measurement in pallet.Measurements)
|
|
{
|
|
measurement.Tag = null;
|
|
measurement.Operator = null;
|
|
|
|
foreach (var point in measurement.Points)
|
|
{
|
|
point.Tag = null;
|
|
point.Verifier = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static TestDataSet CreateDeepNestedTestData()
|
|
{
|
|
TestDataFactory.ResetIdCounter();
|
|
|
|
// IId shared references - only at Order and Item levels for ~10% ratio
|
|
var sharedTag = TestDataFactory.CreateTag("DeepTag");
|
|
var sharedUser = TestDataFactory.CreateUser("deepuser");
|
|
var sharedCategory = TestDataFactory.CreateCategory("DeepCategory");
|
|
|
|
// Non-IId shared reference
|
|
var sharedPreferences = new UserPreferences
|
|
{
|
|
Theme = "light",
|
|
Language = "fr-FR",
|
|
NotificationsEnabled = false,
|
|
EmailDigestFrequency = "monthly"
|
|
};
|
|
sharedUser.Preferences = sharedPreferences;
|
|
|
|
var order = TestDataFactory.CreateOrder(
|
|
itemCount: 2,
|
|
palletsPerItem: 4,
|
|
measurementsPerPallet: 4,
|
|
pointsPerMeasurement: 8,
|
|
sharedTag: sharedTag,
|
|
sharedUser: sharedUser,
|
|
sharedPreferences: sharedPreferences,
|
|
sharedCategory: sharedCategory);
|
|
|
|
// Clear deeper level refs for realistic ~10% ratio
|
|
ClearDeepLevelRefs(order);
|
|
|
|
return new TestDataSet("Deep Nested (2x4x4x8)", order, iidRefPercent: 10);
|
|
}
|
|
|
|
#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)...");
|
|
foreach (var serializer in serializers)
|
|
{
|
|
serializer.Warmup(WarmupIterations);
|
|
}
|
|
|
|
// 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 = false }, SerializerAcBinaryNoIntern),
|
|
|
|
// AcJson
|
|
new AcJsonBenchmark(testData.Order, AcJsonSerializerOptions.Default, SerializerAcJsonDefault),
|
|
|
|
// MessagePack
|
|
new MessagePackBenchmark(testData.Order, SerializerMessagePack),
|
|
|
|
// Newtonsoft.Json
|
|
new NewtonsoftBenchmark(testData.Order, SerializerNewtonsoftJson),
|
|
|
|
// System.Text.Json
|
|
new SystemTextJsonBenchmark(testData.Order, SerializerSystemTextJson)
|
|
};
|
|
}
|
|
|
|
private static double RunTimed(Action action, int iterations)
|
|
{
|
|
var sw = Stopwatch.StartNew();
|
|
for (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);
|
|
}
|
|
|
|
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; }
|
|
|
|
/// <summary>
|
|
/// Percentage of IId shared references in the data (0-100).
|
|
/// Higher values mean more deduplication benefit for Default mode.
|
|
/// </summary>
|
|
public int IIdRefPercent { get; }
|
|
|
|
public TestDataSet(string name, TestOrder order, int iidRefPercent = 0)
|
|
{
|
|
Name = name;
|
|
Order = order;
|
|
IIdRefPercent = iidRefPercent;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets display name including IId ref percentage if set.
|
|
/// </summary>
|
|
public string DisplayName => IIdRefPercent > 0
|
|
? $"{Name} [{IIdRefPercent}% IId refs]"
|
|
: Name;
|
|
}
|
|
|
|
|
|
|
|
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, '─')}┘");
|
|
}
|
|
|
|
// 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();
|
|
|
|
// Serialized bytes for Large test data (AcBinary Default)
|
|
var largeTestData = testDataSets.FirstOrDefault(t => t.Name.StartsWith("Large"));
|
|
if (largeTestData != null)
|
|
{
|
|
sb.AppendLine("=== SERIALIZED BYTES: Large (5x5x5x10) - AcBinary (Default) ===");
|
|
var serializedBytes = AcBinarySerializer.Serialize(largeTestData.Order, AcBinarySerializerOptions.Default);
|
|
sb.AppendLine($"Size: {serializedBytes.Length:N0} bytes");
|
|
sb.AppendLine();
|
|
sb.AppendLine("Hex dump:");
|
|
sb.AppendLine(FormatHexDump(serializedBytes));
|
|
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}");
|
|
}
|
|
|
|
/// <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
|
|
}
|