679 lines
27 KiB
C#
679 lines
27 KiB
C#
using System.Diagnostics;
|
|
using AyCode.Core.Extensions;
|
|
using AyCode.Core.Serializers.Binaries;
|
|
using AyCode.Core.Serializers.Jsons;
|
|
using AyCode.Core.Tests.TestModels;
|
|
using MessagePack;
|
|
using MessagePack.Resolvers;
|
|
|
|
namespace AyCode.Core.Tests.serialization;
|
|
|
|
[TestClass]
|
|
public class QuickBenchmark
|
|
{
|
|
private static readonly MessagePackSerializerOptions MsgPackOptions =
|
|
ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
|
|
|
|
private const int DefaultIterations = 1000;
|
|
|
|
#region Helper Methods
|
|
|
|
private static void PrintBanner(string title)
|
|
{
|
|
Console.WriteLine();
|
|
Console.WriteLine(new string('=', 78));
|
|
Console.WriteLine(title);
|
|
Console.WriteLine(new string('=', 78));
|
|
}
|
|
|
|
private static void PrintTableHeader(string title)
|
|
{
|
|
PrintBanner(title);
|
|
Console.WriteLine($"{"Metric",-25} | {"AcBinary",14} | {"MessagePack",14} | {"Ratio",14}");
|
|
Console.WriteLine(new string('-', 78));
|
|
}
|
|
|
|
private static void PrintTableRow(string metric, double acBinary, double msgPack, string unit = "ms")
|
|
{
|
|
if (msgPack > 0)
|
|
{
|
|
var ratio = acBinary / msgPack;
|
|
var ratioStr = ratio < 1 ? $"{ratio:F2}x faster" : $"{ratio:F2}x slower";
|
|
Console.WriteLine($"{metric,-25} | {acBinary,10:F2} {unit,-2} | {msgPack,10:F2} {unit,-2} | {ratioStr,14}");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"{metric,-25} | {acBinary,10:F2} {unit,-2} | {"N/A",12} | {"(unique)",14}");
|
|
}
|
|
}
|
|
|
|
private static void PrintTableRowSize(string metric, int acBinary, int msgPack)
|
|
{
|
|
var ratio = msgPack == 0 ? 0 : 100.0 * acBinary / msgPack;
|
|
Console.WriteLine($"{metric,-25} | {acBinary,14:N0} | {msgPack,14:N0} | {ratio,12:F1}%");
|
|
}
|
|
|
|
private static void PrintTableFooter()
|
|
{
|
|
Console.WriteLine(new string('-', 78));
|
|
}
|
|
|
|
private static void PrintSummary(int acBinarySize, int msgPackSize, double acSerMs, double msgSerMs, double acDeserMs, double msgDeserMs)
|
|
{
|
|
PrintBanner("SUMMARY");
|
|
|
|
var sizeAdvantage = msgPackSize == 0 ? 0 : 100.0 - (100.0 * acBinarySize / msgPackSize);
|
|
if (sizeAdvantage > 0)
|
|
Console.WriteLine($"[OK] Size: AcBinary is {sizeAdvantage:F1}% smaller ({msgPackSize - acBinarySize:N0} bytes saved)");
|
|
else
|
|
Console.WriteLine($"[WARN] Size: AcBinary is {-sizeAdvantage:F1}% larger");
|
|
|
|
var serRatio = msgSerMs == 0 ? 0 : acSerMs / msgSerMs;
|
|
if (serRatio > 0 && serRatio < 1)
|
|
Console.WriteLine($"[OK] Serialize: AcBinary is {1 / serRatio:F2}x faster");
|
|
else if (serRatio > 0)
|
|
Console.WriteLine($"[WARN] Serialize: AcBinary is {serRatio:F2}x slower");
|
|
|
|
var deserRatio = msgDeserMs == 0 ? 0 : acDeserMs / msgDeserMs;
|
|
if (deserRatio > 0 && deserRatio < 1)
|
|
Console.WriteLine($"[OK] Deserialize: AcBinary is {1 / deserRatio:F2}x faster");
|
|
else if (deserRatio > 0)
|
|
Console.WriteLine($"[WARN] Deserialize: AcBinary is {deserRatio:F2}x slower");
|
|
}
|
|
|
|
private static TestOrder CreatePopulateTarget(TestOrder source)
|
|
{
|
|
var target = new TestOrder { Id = source.Id };
|
|
foreach (var item in source.Items)
|
|
{
|
|
target.Items.Add(new TestOrderItem { Id = item.Id });
|
|
}
|
|
return target;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Basic Benchmarks
|
|
|
|
[TestMethod]
|
|
public void RunQuickBenchmark()
|
|
{
|
|
// Warmup
|
|
var order = TestDataFactory.CreateBenchmarkOrder(2, 2, 2, 3);
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
var bytes = order.ToBinary();
|
|
var result = bytes.BinaryTo<TestOrder>();
|
|
}
|
|
|
|
// Measure serialize
|
|
const int iterations = DefaultIterations;
|
|
var sw = Stopwatch.StartNew();
|
|
byte[] serialized = null!;
|
|
for (int i = 0; i < iterations; i++)
|
|
{
|
|
serialized = order.ToBinary();
|
|
}
|
|
sw.Stop();
|
|
var serializeMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// Measure deserialize
|
|
sw.Restart();
|
|
TestOrder? deserialized = null;
|
|
for (int i = 0; i < iterations; i++)
|
|
{
|
|
deserialized = serialized.BinaryTo<TestOrder>();
|
|
}
|
|
sw.Stop();
|
|
var deserializeMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// JSON comparison
|
|
var jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling();
|
|
sw.Restart();
|
|
string json = null!;
|
|
for (int i = 0; i < iterations; i++)
|
|
{
|
|
json = order.ToJson(jsonOptions);
|
|
}
|
|
sw.Stop();
|
|
var jsonSerializeMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
sw.Restart();
|
|
for (int i = 0; i < iterations; i++)
|
|
{
|
|
var _ = json.JsonTo<TestOrder>(jsonOptions);
|
|
}
|
|
sw.Stop();
|
|
var jsonDeserializeMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
Console.WriteLine($"=== Quick Benchmark ({iterations} iterations) ===");
|
|
Console.WriteLine($"Binary size: {serialized.Length} bytes");
|
|
Console.WriteLine($"JSON size: {json.Length} chars ({System.Text.Encoding.UTF8.GetByteCount(json)} bytes)");
|
|
Console.WriteLine();
|
|
Console.WriteLine($"Binary Serialize: {serializeMs:F2}ms ({serializeMs / iterations:F4}ms/op)");
|
|
Console.WriteLine($"Binary Deserialize: {deserializeMs:F2}ms ({deserializeMs / iterations:F4}ms/op)");
|
|
Console.WriteLine($"JSON Serialize: {jsonSerializeMs:F2}ms ({jsonSerializeMs / iterations:F4}ms/op)");
|
|
Console.WriteLine($"JSON Deserialize: {jsonDeserializeMs:F2}ms ({jsonDeserializeMs / iterations:F4}ms/op)");
|
|
Console.WriteLine();
|
|
Console.WriteLine($"Binary vs JSON Serialize: {serializeMs / jsonSerializeMs:F2}x");
|
|
Console.WriteLine($"Binary vs JSON Deserialize: {deserializeMs / jsonDeserializeMs:F2}x");
|
|
Console.WriteLine($"Size ratio: {100.0 * serialized.Length / System.Text.Encoding.UTF8.GetByteCount(json):F1}%");
|
|
|
|
Assert.IsNotNull(deserialized);
|
|
Assert.AreEqual(order.Id, deserialized.Id);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void RunStringInterningBenchmark()
|
|
{
|
|
// Create data with repeated strings
|
|
var items = Enumerable.Range(0, 100).Select(i => new TestClassWithRepeatedValues
|
|
{
|
|
Id = i,
|
|
Status = i % 3 == 0 ? "Pending" : i % 3 == 1 ? "Processing" : "Completed",
|
|
Category = $"Category_{i % 5}",
|
|
Priority = i % 2 == 0 ? "High_Priority" : "Low_Priority"
|
|
}).ToList();
|
|
|
|
// Warmup
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
var bytes = items.ToBinary();
|
|
var result = bytes.BinaryTo<List<TestClassWithRepeatedValues>>();
|
|
}
|
|
|
|
const int iterations = DefaultIterations;
|
|
|
|
// With interning (default)
|
|
var sw = Stopwatch.StartNew();
|
|
byte[] withInterning = null!;
|
|
for (int i = 0; i < iterations; i++)
|
|
{
|
|
withInterning = AcBinarySerializer.Serialize(items, AcBinarySerializerOptions.Default);
|
|
}
|
|
sw.Stop();
|
|
var withInterningMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// Without interning
|
|
var noInternOptions = new AcBinarySerializerOptions { UseStringInterning = false };
|
|
sw.Restart();
|
|
byte[] withoutInterning = null!;
|
|
for (int i = 0; i < iterations; i++)
|
|
{
|
|
withoutInterning = AcBinarySerializer.Serialize(items, noInternOptions);
|
|
}
|
|
sw.Stop();
|
|
var withoutInterningMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
Console.WriteLine($"=== String Interning Benchmark ({iterations} iterations) ===");
|
|
Console.WriteLine($"With interning: {withInterning.Length} bytes, {withInterningMs:F2}ms");
|
|
Console.WriteLine($"Without interning: {withoutInterning.Length} bytes, {withoutInterningMs:F2}ms");
|
|
Console.WriteLine($"Size savings: {withoutInterning.Length - withInterning.Length} bytes ({100.0 * (withoutInterning.Length - withInterning.Length) / withoutInterning.Length:F1}%)");
|
|
Console.WriteLine($"Speed ratio: {withInterningMs / withoutInterningMs:F2}x");
|
|
|
|
// Verify both deserialize correctly
|
|
var result1 = withInterning.BinaryTo<List<TestClassWithRepeatedValues>>();
|
|
var result2 = withoutInterning.BinaryTo<List<TestClassWithRepeatedValues>>();
|
|
Assert.AreEqual(100, result1!.Count);
|
|
Assert.AreEqual(100, result2!.Count);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region MessagePack Comparison
|
|
|
|
[TestMethod]
|
|
public void RunMessagePackComparison()
|
|
{
|
|
// Create test data
|
|
var order = TestDataFactory.CreateBenchmarkOrder(3, 3, 3, 4);
|
|
|
|
// Warmup
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
var binBytes = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.Default);
|
|
var binResult = AcBinaryDeserializer.Deserialize<TestOrder>(binBytes);
|
|
var msgBytes = MessagePackSerializer.Serialize(order, MsgPackOptions);
|
|
var msgResult = MessagePackSerializer.Deserialize<TestOrder>(msgBytes, MsgPackOptions);
|
|
}
|
|
|
|
const int iterations = DefaultIterations;
|
|
|
|
// === AcBinary Serialize ===
|
|
var sw = Stopwatch.StartNew();
|
|
byte[] acBinaryData = null!;
|
|
for (int i = 0; i < iterations; i++)
|
|
{
|
|
acBinaryData = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.Default);
|
|
}
|
|
sw.Stop();
|
|
var acBinarySerMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// === MessagePack Serialize ===
|
|
sw.Restart();
|
|
byte[] msgPackData = null!;
|
|
for (int i = 0; i < iterations; i++)
|
|
{
|
|
msgPackData = MessagePackSerializer.Serialize(order, MsgPackOptions);
|
|
}
|
|
sw.Stop();
|
|
var msgPackSerMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// === AcBinary Deserialize ===
|
|
sw.Restart();
|
|
TestOrder? acBinaryResult = null;
|
|
for (int i = 0; i < iterations; i++)
|
|
{
|
|
acBinaryResult = AcBinaryDeserializer.Deserialize<TestOrder>(acBinaryData);
|
|
}
|
|
sw.Stop();
|
|
var acBinaryDeserMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// === MessagePack Deserialize ===
|
|
sw.Restart();
|
|
TestOrder? msgPackResult = null;
|
|
for (int i = 0; i < iterations; i++)
|
|
{
|
|
msgPackResult = MessagePackSerializer.Deserialize<TestOrder>(msgPackData, MsgPackOptions);
|
|
}
|
|
sw.Stop();
|
|
var msgPackDeserMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// Print results
|
|
Console.WriteLine($"=== AcBinary vs MessagePack Benchmark ({iterations} iterations) ===");
|
|
Console.WriteLine();
|
|
Console.WriteLine($"{"Metric",-25} {"AcBinary",12} {"MessagePack",12} {"Ratio",10}");
|
|
Console.WriteLine(new string('-', 60));
|
|
Console.WriteLine($"{"Size (bytes)",-25} {acBinaryData.Length,12:N0} {msgPackData.Length,12:N0} {100.0 * acBinaryData.Length / msgPackData.Length,9:F1}%");
|
|
Console.WriteLine($"{"Serialize (ms)",-25} {acBinarySerMs,12:F2} {msgPackSerMs,12:F2} {acBinarySerMs / msgPackSerMs,9:F2}x");
|
|
Console.WriteLine($"{"Deserialize (ms)",-25} {acBinaryDeserMs,12:F2} {msgPackDeserMs,12:F2} {acBinaryDeserMs / msgPackDeserMs,9:F2}x");
|
|
Console.WriteLine($"{"Round-trip (ms)",-25} {acBinarySerMs + acBinaryDeserMs,12:F2} {msgPackSerMs + msgPackDeserMs,12:F2} {(acBinarySerMs + acBinaryDeserMs) / (msgPackSerMs + msgPackDeserMs),9:F2}x");
|
|
Console.WriteLine();
|
|
|
|
var sizeDiff = msgPackData.Length - acBinaryData.Length;
|
|
if (sizeDiff > 0)
|
|
Console.WriteLine($"[OK] AcBinary {sizeDiff:N0} bytes smaller ({100.0 * sizeDiff / msgPackData.Length:F1}% savings)");
|
|
else
|
|
Console.WriteLine($"[WARN] AcBinary {-sizeDiff:N0} bytes larger");
|
|
|
|
Assert.IsNotNull(acBinaryResult);
|
|
Assert.IsNotNull(msgPackResult);
|
|
Assert.AreEqual(order.Id, acBinaryResult.Id);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void RunStringInterningVsMessagePack()
|
|
{
|
|
// Create data with many repeated strings (worst case for MessagePack, best for interning)
|
|
var items = Enumerable.Range(0, 200).Select(i => new TestClassWithRepeatedValues
|
|
{
|
|
Id = i,
|
|
Status = i % 3 == 0 ? "PendingStatus" : i % 3 == 1 ? "ProcessingStatus" : "CompletedStatus",
|
|
Category = $"Category_{i % 5}",
|
|
Priority = i % 2 == 0 ? "HighPriority" : "LowPriority"
|
|
}).ToList();
|
|
|
|
// Warmup
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
var b1 = AcBinarySerializer.Serialize(items, AcBinarySerializerOptions.Default);
|
|
var r1 = AcBinaryDeserializer.Deserialize<List<TestClassWithRepeatedValues>>(b1);
|
|
var b2 = MessagePackSerializer.Serialize(items, MsgPackOptions);
|
|
var r2 = MessagePackSerializer.Deserialize<List<TestClassWithRepeatedValues>>(b2, MsgPackOptions);
|
|
}
|
|
|
|
const int iterations = DefaultIterations;
|
|
|
|
// AcBinary with interning
|
|
var sw = Stopwatch.StartNew();
|
|
byte[] acWithIntern = null!;
|
|
for (int i = 0; i < iterations; i++)
|
|
{
|
|
acWithIntern = AcBinarySerializer.Serialize(items, AcBinarySerializerOptions.Default);
|
|
}
|
|
var acSerMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
sw.Restart();
|
|
for (int i = 0; i < iterations; i++)
|
|
{
|
|
var _ = AcBinaryDeserializer.Deserialize<List<TestClassWithRepeatedValues>>(acWithIntern);
|
|
}
|
|
var acDeserMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// MessagePack
|
|
sw.Restart();
|
|
byte[] msgPack = null!;
|
|
for (int i = 0; i < iterations; i++)
|
|
{
|
|
msgPack = MessagePackSerializer.Serialize(items, MsgPackOptions);
|
|
}
|
|
var msgSerMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
sw.Restart();
|
|
for (int i = 0; i < iterations; i++)
|
|
{
|
|
var _ = MessagePackSerializer.Deserialize<List<TestClassWithRepeatedValues>>(msgPack, MsgPackOptions);
|
|
}
|
|
var msgDeserMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
Console.WriteLine($"=== String Interning Advantage ({iterations} iterations, 200 items with repeated strings) ===");
|
|
Console.WriteLine();
|
|
Console.WriteLine($"{"Metric",-25} {"AcBinary",12} {"MessagePack",12} {"Ratio",10}");
|
|
Console.WriteLine(new string('-', 60));
|
|
Console.WriteLine($"{"Size (bytes)",-25} {acWithIntern.Length,12:N0} {msgPack.Length,12:N0} {100.0 * acWithIntern.Length / msgPack.Length,9:F1}%");
|
|
Console.WriteLine($"{"Serialize (ms)",-25} {acSerMs,12:F2} {msgSerMs,12:F2} {acSerMs / msgSerMs,9:F2}x");
|
|
Console.WriteLine($"{"Deserialize (ms)",-25} {acDeserMs,12:F2} {msgDeserMs,12:F2} {acDeserMs / msgDeserMs,9:F2}x");
|
|
Console.WriteLine();
|
|
|
|
var sizeSaving = msgPack.Length - acWithIntern.Length;
|
|
Console.WriteLine($"[OK] String interning saves {sizeSaving:N0} bytes ({100.0 * sizeSaving / msgPack.Length:F1}%)");
|
|
|
|
Assert.IsTrue(acWithIntern.Length < msgPack.Length, "AcBinary with interning should be smaller");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Full Comparison (WithRef, NoRef, Populate, Merge)
|
|
|
|
[TestMethod]
|
|
public void RunFullBenchmarkComparison()
|
|
{
|
|
PrintBanner("AcBinary vs MessagePack Full Benchmark");
|
|
|
|
// Create test data with shared references
|
|
TestDataFactory.ResetIdCounter();
|
|
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
|
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
|
var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);
|
|
|
|
var testOrder = TestDataFactory.CreateOrder(
|
|
itemCount: 3,
|
|
palletsPerItem: 3,
|
|
measurementsPerPallet: 3,
|
|
pointsPerMeasurement: 4,
|
|
sharedTag: sharedTag,
|
|
sharedUser: sharedUser,
|
|
sharedMetadata: sharedMeta);
|
|
|
|
// Options
|
|
var withRefOptions = new AcBinarySerializerOptions();
|
|
var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
|
|
|
|
// Warmup
|
|
Console.WriteLine("\nWarming up...");
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
_ = AcBinarySerializer.Serialize(testOrder, withRefOptions);
|
|
_ = AcBinarySerializer.Serialize(testOrder, noRefOptions);
|
|
_ = MessagePackSerializer.Serialize(testOrder, MsgPackOptions);
|
|
}
|
|
|
|
// Pre-serialize
|
|
var acBinaryWithRef = AcBinarySerializer.Serialize(testOrder, withRefOptions);
|
|
var acBinaryNoRef = AcBinarySerializer.Serialize(testOrder, noRefOptions);
|
|
var msgPackData = MessagePackSerializer.Serialize(testOrder, MsgPackOptions);
|
|
|
|
Console.WriteLine($"Iterations: {DefaultIterations:N0}");
|
|
|
|
// Size comparison
|
|
PrintTableHeader("SIZE COMPARISON");
|
|
PrintTableRowSize("AcBinary (WithRef)", acBinaryWithRef.Length, msgPackData.Length);
|
|
PrintTableRowSize("AcBinary (NoRef)", acBinaryNoRef.Length, msgPackData.Length);
|
|
PrintTableRowSize("MessagePack (baseline)", msgPackData.Length, msgPackData.Length);
|
|
PrintTableFooter();
|
|
|
|
var sw = Stopwatch.StartNew();
|
|
|
|
// === Serialize WithRef ===
|
|
sw.Restart();
|
|
for (int i = 0; i < DefaultIterations; i++)
|
|
_ = AcBinarySerializer.Serialize(testOrder, withRefOptions);
|
|
var acWithRefSerMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// === Serialize NoRef ===
|
|
sw.Restart();
|
|
for (int i = 0; i < DefaultIterations; i++)
|
|
_ = AcBinarySerializer.Serialize(testOrder, noRefOptions);
|
|
var acNoRefSerMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// === MessagePack Serialize ===
|
|
sw.Restart();
|
|
for (int i = 0; i < DefaultIterations; i++)
|
|
_ = MessagePackSerializer.Serialize(testOrder, MsgPackOptions);
|
|
var msgPackSerMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// === Deserialize WithRef ===
|
|
sw.Restart();
|
|
for (int i = 0; i < DefaultIterations; i++)
|
|
_ = AcBinaryDeserializer.Deserialize<TestOrder>(acBinaryWithRef);
|
|
var acWithRefDeserMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// === Deserialize NoRef ===
|
|
sw.Restart();
|
|
for (int i = 0; i < DefaultIterations; i++)
|
|
_ = AcBinaryDeserializer.Deserialize<TestOrder>(acBinaryNoRef);
|
|
var acNoRefDeserMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// === MessagePack Deserialize ===
|
|
sw.Restart();
|
|
for (int i = 0; i < DefaultIterations; i++)
|
|
_ = MessagePackSerializer.Deserialize<TestOrder>(msgPackData, MsgPackOptions);
|
|
var msgPackDeserMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// === Populate (AcBinary only) ===
|
|
sw.Restart();
|
|
for (int i = 0; i < DefaultIterations; i++)
|
|
{
|
|
var target = CreatePopulateTarget(testOrder);
|
|
AcBinaryDeserializer.Populate(acBinaryNoRef, target);
|
|
}
|
|
var acPopulateMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// === PopulateMerge (AcBinary only) ===
|
|
sw.Restart();
|
|
for (int i = 0; i < DefaultIterations; i++)
|
|
{
|
|
var target = CreatePopulateTarget(testOrder);
|
|
AcBinaryDeserializer.PopulateMerge(acBinaryNoRef.AsSpan(), target);
|
|
}
|
|
var acMergeMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// Print performance table
|
|
PrintTableHeader("PERFORMANCE COMPARISON (lower is better)");
|
|
PrintTableRow("Serialize (WithRef)", acWithRefSerMs, msgPackSerMs);
|
|
PrintTableRow("Serialize (NoRef)", acNoRefSerMs, msgPackSerMs);
|
|
PrintTableRow("Deserialize (WithRef)", acWithRefDeserMs, msgPackDeserMs);
|
|
PrintTableRow("Deserialize (NoRef)", acNoRefDeserMs, msgPackDeserMs);
|
|
PrintTableRow("Populate (NoRef)", acPopulateMs, 0);
|
|
PrintTableRow("Merge (NoRef)", acMergeMs, 0);
|
|
PrintTableRow("Round-trip (WithRef)", acWithRefSerMs + acWithRefDeserMs, msgPackSerMs + msgPackDeserMs);
|
|
PrintTableRow("Round-trip (NoRef)", acNoRefSerMs + acNoRefDeserMs, msgPackSerMs + msgPackDeserMs);
|
|
PrintTableFooter();
|
|
|
|
PrintSummary(acBinaryNoRef.Length, msgPackData.Length, acNoRefSerMs, msgPackSerMs, acNoRefDeserMs, msgPackDeserMs);
|
|
|
|
// Assertions
|
|
Assert.IsTrue(acBinaryWithRef.Length < msgPackData.Length, "AcBinary WithRef should be smaller than MessagePack");
|
|
Assert.IsTrue(acBinaryNoRef.Length < msgPackData.Length, "AcBinary NoRef should be smaller than MessagePack");
|
|
}
|
|
|
|
[TestMethod]
|
|
public void RunWithRefVsNoRefComparison()
|
|
{
|
|
PrintBanner("AcBinary WithRef vs NoRef Comparison");
|
|
|
|
// Create test data WITH shared references (to show WithRef advantage)
|
|
TestDataFactory.ResetIdCounter();
|
|
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
|
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
|
var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);
|
|
|
|
var testOrder = TestDataFactory.CreateOrder(
|
|
itemCount: 5,
|
|
palletsPerItem: 4,
|
|
measurementsPerPallet: 3,
|
|
pointsPerMeasurement: 5,
|
|
sharedTag: sharedTag,
|
|
sharedUser: sharedUser,
|
|
sharedMetadata: sharedMeta);
|
|
|
|
var withRefOptions = new AcBinarySerializerOptions();
|
|
var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling();
|
|
|
|
// Warmup
|
|
Console.WriteLine("Warming up...");
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
_ = AcBinarySerializer.Serialize(testOrder, withRefOptions);
|
|
_ = AcBinarySerializer.Serialize(testOrder, noRefOptions);
|
|
}
|
|
|
|
var withRefData = AcBinarySerializer.Serialize(testOrder, withRefOptions);
|
|
var noRefData = AcBinarySerializer.Serialize(testOrder, noRefOptions);
|
|
|
|
Console.WriteLine($"Iterations: {DefaultIterations:N0}");
|
|
|
|
// Size comparison
|
|
PrintBanner("SIZE COMPARISON (bytes)");
|
|
Console.WriteLine($"WithRef : {withRefData.Length,12:N0} (baseline)");
|
|
var sizeDiff = noRefData.Length - withRefData.Length;
|
|
var ratio = withRefData.Length == 0 ? 0 : 100.0 * noRefData.Length / withRefData.Length;
|
|
Console.WriteLine($"NoRef : {noRefData.Length,12:N0} (diff {sizeDiff:+#;-#;0}) => {ratio:F1}% of WithRef");
|
|
|
|
var sw = Stopwatch.StartNew();
|
|
|
|
// Serialize WithRef
|
|
sw.Restart();
|
|
for (int i = 0; i < DefaultIterations; i++)
|
|
_ = AcBinarySerializer.Serialize(testOrder, withRefOptions);
|
|
var withRefSerMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// Serialize NoRef
|
|
sw.Restart();
|
|
for (int i = 0; i < DefaultIterations; i++)
|
|
_ = AcBinarySerializer.Serialize(testOrder, noRefOptions);
|
|
var noRefSerMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// Deserialize WithRef
|
|
sw.Restart();
|
|
for (int i = 0; i < DefaultIterations; i++)
|
|
_ = AcBinaryDeserializer.Deserialize<TestOrder>(withRefData);
|
|
var withRefDeserMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// Deserialize NoRef
|
|
sw.Restart();
|
|
for (int i = 0; i < DefaultIterations; i++)
|
|
_ = AcBinaryDeserializer.Deserialize<TestOrder>(noRefData);
|
|
var noRefDeserMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
PrintBanner("PERFORMANCE COMPARISON (ms)");
|
|
Console.WriteLine($"Serialize -> WithRef: {withRefSerMs,8:F2} | NoRef: {noRefSerMs,8:F2}");
|
|
Console.WriteLine($"Deserialize-> WithRef: {withRefDeserMs,8:F2} | NoRef: {noRefDeserMs,8:F2}");
|
|
Console.WriteLine($"Round-trip -> WithRef: {withRefSerMs + withRefDeserMs,8:F2} | NoRef: {noRefSerMs + noRefDeserMs,8:F2}");
|
|
|
|
if (withRefData.Length < noRefData.Length)
|
|
{
|
|
Console.WriteLine($"[OK] WithRef saves {noRefData.Length - withRefData.Length:N0} bytes by deduplicating shared references.");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"[INFO] NoRef saves {withRefData.Length - noRefData.Length:N0} bytes because it skips reference metadata.");
|
|
}
|
|
|
|
// Verify correctness
|
|
var resultWithRef = AcBinaryDeserializer.Deserialize<TestOrder>(withRefData);
|
|
var resultNoRef = AcBinaryDeserializer.Deserialize<TestOrder>(noRefData);
|
|
Assert.IsNotNull(resultWithRef);
|
|
Assert.IsNotNull(resultNoRef);
|
|
Assert.AreEqual(testOrder.Id, resultWithRef.Id);
|
|
Assert.AreEqual(testOrder.Id, resultNoRef.Id);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void RunPopulateAndMergeBenchmark()
|
|
{
|
|
PrintBanner("AcBinary Populate & Merge Benchmark");
|
|
|
|
TestDataFactory.ResetIdCounter();
|
|
var testOrder = TestDataFactory.CreateBenchmarkOrder(
|
|
itemCount: 3,
|
|
palletsPerItem: 3,
|
|
measurementsPerPallet: 3,
|
|
pointsPerMeasurement: 4);
|
|
|
|
var options = AcBinarySerializerOptions.WithoutReferenceHandling();
|
|
var binaryData = AcBinarySerializer.Serialize(testOrder, options);
|
|
|
|
Console.WriteLine("Warming up...");
|
|
for (int i = 0; i < 50; i++)
|
|
{
|
|
var target = CreatePopulateTarget(testOrder);
|
|
AcBinaryDeserializer.Populate(binaryData, target);
|
|
AcBinaryDeserializer.PopulateMerge(binaryData.AsSpan(), target);
|
|
}
|
|
|
|
Console.WriteLine($"Iterations: {DefaultIterations:N0}");
|
|
Console.WriteLine($"Data size: {binaryData.Length:N0} bytes");
|
|
|
|
var sw = Stopwatch.StartNew();
|
|
|
|
// Deserialize (creates new object)
|
|
sw.Restart();
|
|
for (int i = 0; i < DefaultIterations; i++)
|
|
_ = AcBinaryDeserializer.Deserialize<TestOrder>(binaryData);
|
|
var deserializeMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// Populate (reuses existing object)
|
|
sw.Restart();
|
|
for (int i = 0; i < DefaultIterations; i++)
|
|
{
|
|
var target = CreatePopulateTarget(testOrder);
|
|
AcBinaryDeserializer.Populate(binaryData, target);
|
|
}
|
|
var populateMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// PopulateMerge (IId-based merge)
|
|
sw.Restart();
|
|
for (int i = 0; i < DefaultIterations; i++)
|
|
{
|
|
var target = CreatePopulateTarget(testOrder);
|
|
AcBinaryDeserializer.PopulateMerge(binaryData.AsSpan(), target);
|
|
}
|
|
var mergeMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
// PopulateMerge with RemoveOrphanedItems
|
|
var mergeWithRemoveOptions = new AcBinarySerializerOptions { RemoveOrphanedItems = true };
|
|
sw.Restart();
|
|
for (int i = 0; i < DefaultIterations; i++)
|
|
{
|
|
var target = CreatePopulateTarget(testOrder);
|
|
AcBinaryDeserializer.PopulateMerge(binaryData.AsSpan(), target, mergeWithRemoveOptions);
|
|
}
|
|
var mergeWithRemoveMs = sw.Elapsed.TotalMilliseconds;
|
|
|
|
PrintBanner("OPERATION COMPARISON (ms)");
|
|
Console.WriteLine($"Deserialize (new object): {deserializeMs:F2} (baseline)");
|
|
Console.WriteLine($"Populate (reuse obj) : {populateMs:F2} ({populateMs / deserializeMs:F2}x of baseline)");
|
|
Console.WriteLine($"PopulateMerge : {mergeMs:F2} ({mergeMs / deserializeMs:F2}x of baseline)");
|
|
Console.WriteLine($"PopulateMerge + cleanup: {mergeWithRemoveMs:F2} ({mergeWithRemoveMs / deserializeMs:F2}x of baseline)");
|
|
|
|
Console.WriteLine("[INFO] Populate/Merge reuse existing objects - ideal for UI data binding scenarios.");
|
|
|
|
Assert.IsTrue(true); // Test passed if no exceptions
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Test Models
|
|
|
|
public class TestClassWithRepeatedValues
|
|
{
|
|
public int Id { get; set; }
|
|
public string Status { get; set; } = "";
|
|
public string Category { get; set; } = "";
|
|
public string Priority { get; set; } = "";
|
|
}
|
|
|
|
#endregion
|
|
}
|