Refactor: BDN runner, unified reporting, doc overhaul
- Introduce BDN-based runner (AcBinaryVsMemPackBenchmark) mirroring Console's FastestByte scenario; add BdnSummaryAdapter for unified result translation. - Standardize output: both runners emit .log/.LLM/.output triplets; BDN-native artifacts go under Benchmark/BDN/. - Simplify CLI: replace granular switches with --serializers; update help and usage. - Remove legacy benchmark classes; focus on scenario-based approach. - Rewrite README.md for both AyCode.Benchmark and Console to document dual-runner architecture, output conventions, and dependencies. - Rotate BINARY_TODO.md; archive closed entries to BINARY_TODO_2026_04.md and BINARY_TODO_2026_05.md. - Add BINARY_SGEN_OPTIMIZATION.md for SGen per-property emit optimization notes. - Update comments and docstrings for clarity and maintainability; clarify BenchmarkResult iteration semantics for BDN rows.
This commit is contained in:
parent
ed03d754ec
commit
c611d4b535
|
|
@ -0,0 +1,79 @@
|
|||
using AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||
using AyCode.Core.Serializers;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
|
||||
namespace AyCode.Core.Benchmarks;
|
||||
|
||||
/// <summary>
|
||||
/// BDN benchmark mirroring the Console app's "F" menu (<c>SerializerSelectionMode.FastestByte</c>) —
|
||||
/// the focused 1:1 comparison between <b>AcBinary FastMode Byte[]</b> and <b>MemoryPack Default Byte[]</b>
|
||||
/// across the 5 production-shaped test data cells (Small / Medium / Large / Repeated / Deep).
|
||||
///
|
||||
/// <para>Why this exists: the Console app's adaptive measurement engine gives fast turnaround but is
|
||||
/// noise-prone; BDN's warmup + iteration + outlier-removal stack tightens the inter-engine delta to
|
||||
/// the point where ~1-2% micro-optimizations become detectable. Both runners feed the SAME
|
||||
/// <see cref="ISerializerBenchmark"/>-implementing workload (<see cref="AcBinaryBenchmark{T}"/> /
|
||||
/// <see cref="MemoryPackBenchmark{T}"/>) — so the BDN numbers are directly comparable to Console's
|
||||
/// <c>Console.FullBenchmark_Release_*.LLM</c> rows, only with tighter confidence intervals.</para>
|
||||
///
|
||||
/// <para>Output: BDN writes its native artifacts to <c>Test_Benchmark_Results/Benchmark/BDN/</c> (set
|
||||
/// globally in <c>Program.cs</c> via <c>WithArtifactsPath</c>). <see cref="BdnSummaryAdapter"/> then
|
||||
/// translates the <see cref="BenchmarkDotNet.Reports.Summary"/> into <see cref="Reporting.BenchmarkResult"/>
|
||||
/// rows and emits the unified <c>Bdn.FullBenchmark_*.{log,LLM,output}</c> triplet next to Console's
|
||||
/// counterparts in <c>Test_Benchmark_Results/Benchmark/</c>.</para>
|
||||
/// </summary>
|
||||
[MemoryDiagnoser]
|
||||
public class AcBinaryVsMemPackBenchmark
|
||||
{
|
||||
/// <summary>
|
||||
/// The 5 TestData cells matching Console's <c>BenchmarkLayer.Core</c> set —
|
||||
/// Small (2x2x2x2) / Medium (3x3x3x4) / Large (5x5x5x10) / Repeated (10 items) / Deep (2x4x4x8).
|
||||
/// Resolved at <see cref="GlobalSetup"/> time via <see cref="BenchmarkTestDataProvider_All_False.CreateTestDataSets"/>
|
||||
/// (same provider Console uses) so the workload graphs are bit-for-bit identical.
|
||||
/// </summary>
|
||||
public static IEnumerable<string> TestDataNames => new[] { "Small", "Medium", "Large", "Repeated", "Deep" };
|
||||
|
||||
[ParamsSource(nameof(TestDataNames))]
|
||||
public string TestData { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Engine axis: AcBinary FastMode + Compact wire (UTF-8) vs MemoryPack Default (UTF-8). Compact-on-both-sides
|
||||
/// keeps the string-encoding dimension constant so the comparison reflects engine differences only.
|
||||
/// </summary>
|
||||
[Params("AcBinary", "MemoryPack")]
|
||||
public string Engine { get; set; } = "";
|
||||
|
||||
private ISerializerBenchmark _serializer = null!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
var allTestData = BenchmarkTestDataProvider_All_False.CreateTestDataSets();
|
||||
var testDataSet = (TestDataSet<TestOrder_All_False>)allTestData.First(t => t.Name.StartsWith(TestData));
|
||||
|
||||
if (Engine == "AcBinary")
|
||||
{
|
||||
var options = AcBinarySerializerOptions.FastMode;
|
||||
options.WireMode = WireMode.Compact;
|
||||
_serializer = new AcBinaryBenchmark<TestOrder_All_False>(testDataSet.Order, options, "FastMode");
|
||||
}
|
||||
else
|
||||
{
|
||||
// MemoryPack's wire-mode-aligned ctor — Compact ↔ UTF-8 default for apples-to-apples vs AcBinary Compact.
|
||||
_serializer = new MemoryPackBenchmark<TestOrder_All_False>(testDataSet.Order, WireMode.Compact, "Default");
|
||||
}
|
||||
|
||||
// Round-trip correctness check before the BDN harness starts measuring — same gate the Console
|
||||
// runner enforces. Fails the run early if anything's broken (rather than producing meaningless numbers).
|
||||
if (!_serializer.VerifyRoundTrip())
|
||||
throw new InvalidOperationException($"Round-trip verification FAILED for {Engine} on {TestData}.");
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Serialize() => _serializer.Serialize();
|
||||
|
||||
[Benchmark]
|
||||
public void Deserialize() => _serializer.Deserialize();
|
||||
}
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
using AyCode.Core.Benchmarks.Reporting;
|
||||
using AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||
using AyCode.Core.Serializers;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
using BenchmarkDotNet.Reports;
|
||||
using System.Text;
|
||||
|
||||
namespace AyCode.Core.Benchmarks;
|
||||
|
||||
/// <summary>
|
||||
/// Translates <see cref="BenchmarkDotNet.Reports.Summary"/> (BDN's post-run aggregate) into the unified
|
||||
/// <see cref="BenchmarkResult"/> rows consumed by <see cref="BenchmarkReportWriter"/>, then emits the
|
||||
/// <c>Bdn.FullBenchmark_*.{log,LLM,output}</c> triplet alongside Console's counterparts in
|
||||
/// <see cref="ReportingContext.ResolveResultsDirectory"/>'s <c>Test_Benchmark_Results/Benchmark/</c>.
|
||||
///
|
||||
/// <para><b>Why a separate adapter</b>: BDN's Summary is per-method (Serialize / Deserialize as separate
|
||||
/// <see cref="BenchmarkReport"/>s, parameterised by TestData + Engine). The unified format collapses these
|
||||
/// into per-cell rows (one row per <c>TestData × Engine</c> with both Ser and Des stats inline). The adapter
|
||||
/// groups, transposes, and converts ns → ms before handing off to the shared writer.</para>
|
||||
///
|
||||
/// <para><b>Mean vs Median</b>: maps BDN's <see cref="BenchmarkDotNet.Mathematics.Statistics.Median"/> into
|
||||
/// the BenchmarkResult's time columns — same convention as Console (which captures sample-median).
|
||||
/// Min/Max/StdDev populate the inter-sample range surfaced in <see cref="BenchmarkReportWriter.FormatMicrosWithRange"/>
|
||||
/// (incl. CV-warning ⚠️ marker when stddev/median exceeds <see cref="ReportingContext.UnstableCVThreshold"/>).</para>
|
||||
///
|
||||
/// <para><b>Iteration count = 1</b>: BDN reports <i>per-operation</i> time (ns) — already amortized across N
|
||||
/// invocations. The unified BenchmarkResult expects total-batch time + iteration count (so <c>µs/op =
|
||||
/// timeMs / iterations * 1000</c>). Storing Mean-in-ms with iterations = 1 makes the same formula yield
|
||||
/// Mean-in-µs directly. The actual BDN N count is recorded in the BDN-native artifacts (<c>.../BDN/...</c>)
|
||||
/// for anyone who wants the raw invocation count.</para>
|
||||
/// </summary>
|
||||
public static class BdnSummaryAdapter
|
||||
{
|
||||
/// <summary>
|
||||
/// Post-run entry point — call once after <c>BenchmarkRunner.Run<AcBinaryVsMemPackBenchmark>(...)</c>
|
||||
/// returns. Produces the BDN-side <c>Bdn.*</c> file triplet AND prints the grouped-results console table
|
||||
/// (same view Console produces post-run) so the user sees the cell-level deltas immediately, without
|
||||
/// having to open the .LLM file.
|
||||
/// </summary>
|
||||
public static void WriteResults(Summary summary)
|
||||
{
|
||||
var allTestData = BenchmarkTestDataProvider_All_False.CreateTestDataSets();
|
||||
var results = Translate(summary, allTestData);
|
||||
var ctx = CreateContext();
|
||||
|
||||
BenchmarkReportWriter.PrintGroupedResults(results, allTestData);
|
||||
BenchmarkReportWriter.SaveAll(ctx, results, allTestData);
|
||||
}
|
||||
|
||||
private static ReportingContext CreateContext()
|
||||
{
|
||||
#if DEBUG
|
||||
const string buildConfig = "Debug";
|
||||
#else
|
||||
const string buildConfig = "Release";
|
||||
#endif
|
||||
return new ReportingContext(
|
||||
SourceTag: "Bdn",
|
||||
ResultsDirectory: ReportingContext.ResolveResultsDirectory(),
|
||||
BuildConfiguration: buildConfig,
|
||||
Utf8NoBom: new UTF8Encoding(encoderShouldEmitUTF8Identifier: false),
|
||||
CharsetName: GetCharsetName(),
|
||||
// Warmup / Samples / TargetSampleMs are BDN-managed (not Console's adaptive engine). Zeros here
|
||||
// signal "BDN handled internally" in the header; the BDN-native artifacts under .../BDN/ have
|
||||
// the exact BDN config (warmup count, iteration count, run strategy) for anyone who needs it.
|
||||
WarmupIterations: 0,
|
||||
BenchmarkSamples: 0,
|
||||
TargetSampleMs: 0,
|
||||
UnstableCVThreshold: 0.03);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up the human-readable name for the currently-active <see cref="BenchmarkTestDataProvider.LongStringSuffix"/>
|
||||
/// charset. Mirrors Console's <c>Configuration.GetCurrentCharsetName</c> — Console's <c>Menu</c> sets the
|
||||
/// charset before invoking the bench; for BDN the default charset (Latin1Long) is in effect unless the user
|
||||
/// overrides at runtime.
|
||||
/// </summary>
|
||||
private static string GetCharsetName()
|
||||
{
|
||||
var s = BenchmarkTestDataProvider.LongStringSuffix;
|
||||
return s switch
|
||||
{
|
||||
CharsetSuffixes.Latin1FixAscii => "Latin1FixAscii",
|
||||
CharsetSuffixes.Latin1Short => "Latin1Short",
|
||||
CharsetSuffixes.Latin1Long => "Latin1Long",
|
||||
CharsetSuffixes.CjkBmp => "CjkBmp",
|
||||
CharsetSuffixes.Cyrillic => "Cyrillic",
|
||||
CharsetSuffixes.Mixed => "Mixed",
|
||||
_ => "Custom"
|
||||
};
|
||||
}
|
||||
|
||||
private static List<BenchmarkResult> Translate(Summary summary, List<TestDataSet> allTestData)
|
||||
{
|
||||
var grouped = summary.Reports
|
||||
.Where(r => r.Success && r.ResultStatistics != null)
|
||||
.GroupBy(r => (
|
||||
TestData: GetParam(r, "TestData"),
|
||||
Engine: GetParam(r, "Engine")
|
||||
))
|
||||
.Where(g => !string.IsNullOrEmpty(g.Key.TestData) && !string.IsNullOrEmpty(g.Key.Engine))
|
||||
.ToList();
|
||||
|
||||
var results = new List<BenchmarkResult>(grouped.Count);
|
||||
|
||||
foreach (var group in grouped)
|
||||
{
|
||||
var testDataSet = (TestDataSet<TestOrder_All_False>)allTestData.First(t => t.Name.StartsWith(group.Key.TestData));
|
||||
var engineEnum = group.Key.Engine switch
|
||||
{
|
||||
"AcBinary" => BenchmarkEngine.AcBinary,
|
||||
"MemoryPack" => BenchmarkEngine.MemoryPack,
|
||||
_ => throw new InvalidOperationException($"Unknown engine in BDN params: {group.Key.Engine}")
|
||||
};
|
||||
|
||||
// Construct the same workload instance AcBinaryVsMemPackBenchmark.Setup would build — same options,
|
||||
// same wire mode. Reading SerializedSize + OptionsDescription from it keeps the BDN-side metadata
|
||||
// in lockstep with what the workload actually serialised (no drift between hardcoded BDN strings
|
||||
// and the workload's own OptionsDescription / SerializedSize).
|
||||
var workload = CreateWorkload(testDataSet, group.Key.Engine);
|
||||
|
||||
var result = new BenchmarkResult
|
||||
{
|
||||
TestDataName = testDataSet.DisplayName,
|
||||
Engine = engineEnum,
|
||||
IoMode = BenchmarkIoMode.ByteArray,
|
||||
DispatchMode = BenchmarkDispatchMode.SGen,
|
||||
OptionsPreset = group.Key.Engine == "AcBinary" ? "FastMode" : "Default",
|
||||
OrderTypeName = nameof(TestOrder_All_False),
|
||||
SerializedSize = workload.SerializedSize,
|
||||
OptionsDescription = workload.OptionsDescription,
|
||||
};
|
||||
|
||||
// ns → ms (BenchmarkResult expects ms per op with iter=1, so µs/op = ms * 1000 / 1 = ms*1000).
|
||||
const double nsToMs = 1.0 / 1_000_000.0;
|
||||
|
||||
foreach (var report in group)
|
||||
{
|
||||
var methodName = report.BenchmarkCase.Descriptor.WorkloadMethod.Name;
|
||||
var stats = report.ResultStatistics!;
|
||||
var allocBytes = report.GcStats.GetBytesAllocatedPerOperation(report.BenchmarkCase) ?? 0;
|
||||
|
||||
if (methodName == "Serialize")
|
||||
{
|
||||
result.SerializeTimeMs = stats.Median * nsToMs;
|
||||
result.SerializeTimeMinMs = stats.Min * nsToMs;
|
||||
result.SerializeTimeMaxMs = stats.Max * nsToMs;
|
||||
result.SerializeTimeStdDevMs = stats.StandardDeviation * nsToMs;
|
||||
result.SerializeIterations = 1; // see class-doc "Iteration count = 1" note
|
||||
result.SerializeAllocBytesPerOp = allocBytes;
|
||||
}
|
||||
else if (methodName == "Deserialize")
|
||||
{
|
||||
result.DeserializeTimeMs = stats.Median * nsToMs;
|
||||
result.DeserializeTimeMinMs = stats.Min * nsToMs;
|
||||
result.DeserializeTimeMaxMs = stats.Max * nsToMs;
|
||||
result.DeserializeTimeStdDevMs = stats.StandardDeviation * nsToMs;
|
||||
result.DeserializeIterations = 1;
|
||||
result.DeserializeAllocBytesPerOp = allocBytes;
|
||||
}
|
||||
}
|
||||
|
||||
// Compose RT from Ser + Des per-op µs (same logic as Console BenchmarkLoop's in-memory
|
||||
// composition — since BDN measures Ser and Des independently, RT here is the analytic sum).
|
||||
var serPerOp = BenchmarkReportWriter.ToPerOpMicros(result.SerializeTimeMs, result.SerializeIterations);
|
||||
var desPerOp = BenchmarkReportWriter.ToPerOpMicros(result.DeserializeTimeMs, result.DeserializeIterations);
|
||||
var rtPerOp = serPerOp + desPerOp;
|
||||
result.RoundTripIterations = Math.Max(result.SerializeIterations, result.DeserializeIterations);
|
||||
result.RoundTripTimeMs = rtPerOp / 1000.0 * result.RoundTripIterations;
|
||||
result.RoundTripAllocBytesPerOp = result.SerializeAllocBytesPerOp + result.DeserializeAllocBytesPerOp;
|
||||
|
||||
results.Add(result);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static string GetParam(BenchmarkReport report, string name) =>
|
||||
report.BenchmarkCase.Parameters.Items.FirstOrDefault(p => p.Name == name)?.Value?.ToString() ?? "";
|
||||
|
||||
/// <summary>
|
||||
/// Constructs the same workload instance <see cref="AcBinaryVsMemPackBenchmark.Setup"/> would build —
|
||||
/// same options, same wire mode. The adapter reads <see cref="ISerializerBenchmark.SerializedSize"/> and
|
||||
/// <see cref="ISerializerBenchmark.OptionsDescription"/> from this instance so the BDN-side BenchmarkResult
|
||||
/// rows carry the same workload-side metadata the Console rows have (no risk of drift between hardcoded
|
||||
/// adapter strings and what the workload actually used).
|
||||
///
|
||||
/// Cost: one Serialize call inside the ctor per (TestData × Engine) cell — runs once during summary
|
||||
/// translation, NOT in BDN's measured hot path. Negligible vs BDN's per-run cost.
|
||||
/// </summary>
|
||||
private static ISerializerBenchmark CreateWorkload(TestDataSet<TestOrder_All_False> testDataSet, string engine)
|
||||
{
|
||||
if (engine == "AcBinary")
|
||||
{
|
||||
var options = AcBinarySerializerOptions.FastMode;
|
||||
options.WireMode = WireMode.Compact;
|
||||
return new AcBinaryBenchmark<TestOrder_All_False>(testDataSet.Order, options, "FastMode");
|
||||
}
|
||||
return new MemoryPackBenchmark<TestOrder_All_False>(testDataSet.Order, WireMode.Compact, "Default");
|
||||
}
|
||||
}
|
||||
|
|
@ -70,9 +70,15 @@ namespace AyCode.Benchmark
|
|||
return;
|
||||
}
|
||||
|
||||
// Configure BenchmarkDotNet to write artifacts into the centralized benchmark directory
|
||||
// BDN-native artifacts go under <results>/Benchmark/BDN/ (per the unified output convention —
|
||||
// see ReportingContext docs). The unified Bdn.FullBenchmark_*.{log,LLM,output} triplet (emitted
|
||||
// by BdnSummaryAdapter after BDN finishes) lands one level up in <results>/Benchmark/, next to
|
||||
// the Console.*.* counterparts produced by the Console runner.
|
||||
var bdnArtifactsDir = Path.Combine(benchmarkDir, "BDN");
|
||||
Directory.CreateDirectory(bdnArtifactsDir);
|
||||
|
||||
var config = ManualConfig.Create(DefaultConfig.Instance)
|
||||
.WithArtifactsPath(benchmarkDir);
|
||||
.WithArtifactsPath(bdnArtifactsDir);
|
||||
|
||||
if (args.Length > 0 && args[0] == "--quick")
|
||||
{
|
||||
|
|
@ -94,33 +100,14 @@ namespace AyCode.Benchmark
|
|||
return;
|
||||
}
|
||||
|
||||
if (args.Length > 0 && args[0] == "--minimal")
|
||||
if (args.Length > 0 && args[0] == "--serializers")
|
||||
{
|
||||
RunBenchmark<MinimalBenchmark>(config, benchmarkDir, memDiagDir, "MinimalBenchmark");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length > 0 && args[0] == "--simple")
|
||||
{
|
||||
RunBenchmark<SimpleBinaryBenchmark>(config, benchmarkDir, memDiagDir, "SimpleBinaryBenchmark");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length > 0 && args[0] == "--complex")
|
||||
{
|
||||
RunBenchmark<ComplexBinaryBenchmark>(config, benchmarkDir, memDiagDir, "ComplexBinaryBenchmark");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length > 0 && args[0] == "--msgpack")
|
||||
{
|
||||
RunBenchmark<MessagePackComparisonBenchmark>(config, benchmarkDir, memDiagDir, "MessagePackComparisonBenchmark");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length > 0 && args[0] == "--sizes")
|
||||
{
|
||||
RunSizeComparison();
|
||||
// Unified serializer benchmark mirroring Console's "F" menu (FastestByte) — AcBinary FastMode
|
||||
// Byte[] vs MemoryPack Default Byte[] across 5 TestData cells. BdnSummaryAdapter translates
|
||||
// the BDN Summary into BenchmarkResult rows and emits the Bdn.FullBenchmark_*.{log,LLM,output}
|
||||
// triplet to <results>/Benchmark/ (BDN-native artifacts go under .../BDN/ via the global config).
|
||||
var serializerSummary = BenchmarkRunner.Run<AcBinaryVsMemPackBenchmark>(config);
|
||||
BdnSummaryAdapter.WriteResults(serializerSummary);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -134,25 +121,16 @@ namespace AyCode.Benchmark
|
|||
Console.WriteLine(" --quick Quick benchmark with tabular output (AcBinary vs MessagePack)");
|
||||
Console.WriteLine(" --test Quick AcBinary test");
|
||||
Console.WriteLine(" --testmsgpack Quick MessagePack test");
|
||||
Console.WriteLine(" --minimal Minimal benchmark");
|
||||
Console.WriteLine(" --simple Simple flat object benchmark");
|
||||
Console.WriteLine(" --complex Complex hierarchy (AcBinary vs JSON)");
|
||||
Console.WriteLine(" --msgpack MessagePack comparison");
|
||||
Console.WriteLine(" --sizes Size comparison only");
|
||||
Console.WriteLine(" --serializers AcBinary FastMode vs MemoryPack Default across 5 test data cells (mirrors Console F menu)");
|
||||
Console.WriteLine(" --jitasm JIT disassembly analysis (shows actual x64 assembly for hot path)");
|
||||
Console.WriteLine(" --save-coverage <file> Save coverage file into Test_Benchmark_Results/CoverageReport");
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
BenchmarkSwitcher.FromAssembly(typeof(MinimalBenchmark).Assembly).Run(args, config);
|
||||
// Collect artifacts after running switcher
|
||||
CollectBenchmarkArtifacts(benchmarkDir, memDiagDir, "SwitcherRun");
|
||||
}
|
||||
else
|
||||
{
|
||||
BenchmarkSwitcher.FromAssembly(typeof(MinimalBenchmark).Assembly).Run(args, config);
|
||||
CollectBenchmarkArtifacts(benchmarkDir, memDiagDir, "SwitcherRun");
|
||||
}
|
||||
// Default path: hand control to BDN's BenchmarkSwitcher (no args → interactive picker; with
|
||||
// args → BDN parses them as benchmark filters / job options). Same code path either way — the
|
||||
// known custom switches above (--serializers, --jitasm, --quick, --test, --testmsgpack,
|
||||
// --save-coverage) return early before reaching this point.
|
||||
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);
|
||||
CollectBenchmarkArtifacts(benchmarkDir, memDiagDir, "SwitcherRun");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,129 @@
|
|||
# AyCode.Benchmark
|
||||
|
||||
BenchmarkDotNet-based performance benchmarking console app. Compares AcBinary serializer against MessagePack, BSON, and JSON across various scenarios.
|
||||
BenchmarkDotNet performance suite **plus** the shared workload / reporting infrastructure used by both BDN and the Console runner. Targets .NET 9.
|
||||
|
||||
## Key Files
|
||||
## Role: dual-purpose project
|
||||
|
||||
- **`Program.cs`** — CLI entry point with `--quick`, `--test`, `--minimal`, `--simple`, `--complex`, `--msgpack`, `--sizes`, `--jitasm` modes. Collects results to `Test_Benchmark_Results/` at solution root.
|
||||
- **`SerializationBenchmarks.cs`** — Primary suite: MinimalBenchmark, SimpleBinaryBenchmark, ComplexBinaryBenchmark, MessagePackComparisonBenchmark, AcBinaryVsMessagePackFullBenchmark, SizeComparisonBenchmark, LargeScaleBenchmark (~25K objects), AcJsonVsSystemTextJsonBenchmark.
|
||||
- **`SourceGeneratorBenchmarks.cs`** — Source-generated vs runtime reflection serializers. Includes PureContractlessBenchmark, SourceGeneratorVsRuntimeBenchmark, RepeatedStringBenchmark (string interning).
|
||||
- **`SignalRCommunicationBenchmarks.cs`** — Full-stack SignalR message performance: client creation → MessagePack serialization → server deserialization → response → round-trip.
|
||||
- **`SignalRRoundTripBenchmarks.cs`** — Real SignalR infrastructure benchmarks: primitives, complex objects, collections, mixed parameters.
|
||||
- **`JitDisassemblyBenchmark.cs`** — JIT analysis: generates .asm files to verify inlining decisions on serialize/deserialize hot paths.
|
||||
- **`TaskHelperBenchmarks.cs`** — Task/timing utilities: WaitToAsync, ThreadPool (custom vs Task.Run), timing methods (UtcNow.Ticks vs TickCount64).
|
||||
- **`RefForeachBenchmark.cs`** — Collection iteration patterns: array vs list, foreach vs index, ref readonly vs by-value for large structs.
|
||||
- **`ValueTypePassingBenchmark.cs`** — Copy-by-value vs `in` parameter for 16-byte types (Decimal, DateTimeOffset, Guid).
|
||||
This project plays **two roles**:
|
||||
|
||||
1. **BDN runner Exe** — standalone benchmark host (`Program.cs` + `[Benchmark]`-decorated classes). Invoke via `dotnet run -c Release --project AyCode.Benchmark -- <switch>`.
|
||||
2. **Shared workload + reporting library** — exposes `public` types under [`Workloads/Scenarios/`](Workloads/Scenarios/) and [`Reporting/`](Reporting/) that [`AyCode.Core.Serializers.Console`](../AyCode.Core.Serializers.Console/README.md) consumes via `<ProjectReference>`.
|
||||
|
||||
Both runners feed the SAME `ISerializerBenchmark` workload (same test data graphs, same wire options, same payload sizes) — so Console's adaptive-engine numbers and BDN's iteration-based numbers are **directly comparable**.
|
||||
|
||||
## Output convention
|
||||
|
||||
Both runners emit a unified `.log` / `.LLM` / `.output` triplet to `Test_Benchmark_Results/Benchmark/` (resolved at runtime via walk-up to the nearest `AyCode.Core.sln` — worktree-aware):
|
||||
|
||||
| File | Source | Content |
|
||||
|---|---|---|
|
||||
| `Console.FullBenchmark_<Build>_<ts>.log` | Console runner | Human-readable formatted view |
|
||||
| `Console.FullBenchmark_<Build>_<ts>.LLM` | Console runner | Markdown table, LLM-paste-friendly |
|
||||
| `Console.FullBenchmark_<Build>_<ts>.output` | Console runner | Hex dump of Large cell binary |
|
||||
| `Bdn.FullBenchmark_<Build>_<ts>.log` | BDN runner | Same format as Console |
|
||||
| `Bdn.FullBenchmark_<Build>_<ts>.LLM` | BDN runner | Same |
|
||||
| `Bdn.FullBenchmark_<Build>_<ts>.output` | BDN runner | Same |
|
||||
|
||||
BDN-native artifacts (BDN's own reports, raw measurements, run logs) go to `Test_Benchmark_Results/Benchmark/BDN/` — kept separate so the unified Console+BDN `.log/.LLM/.output` triplet stays uncluttered.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ AyCode.Benchmark (this project) │
|
||||
│ │
|
||||
│ Workloads/Scenarios/ public — shared workload types │
|
||||
│ ISerializerBenchmark, BenchmarkOptions, BenchmarkEnums, │
|
||||
│ AcBinaryBenchmark<T>, MemoryPackBenchmark<T>, │
|
||||
│ AcBinaryBufferWriterBenchmark<T>, ... (12 concretes), │
|
||||
│ RoundTripValidator │
|
||||
│ │
|
||||
│ Reporting/ public — shared reporting types │
|
||||
│ BenchmarkResult, ReportingContext, BenchmarkReportWriter │
|
||||
│ │
|
||||
│ AcBinaryVsMemPackBenchmark.cs BDN [Benchmark] class │
|
||||
│ (mirrors Console "F" menu) │
|
||||
│ BdnSummaryAdapter.cs Summary → BenchmarkResult → │
|
||||
│ BenchmarkReportWriter.SaveAll │
|
||||
│ Program.cs BDN entry + CLI dispatch │
|
||||
│ │
|
||||
│ + KEEP: JitDisassemblyBenchmark, RefForeachBenchmark, │
|
||||
│ TaskHelperBenchmarks, ValueTypePassingBenchmark, │
|
||||
│ SourceGeneratorBenchmarks, │
|
||||
│ SignalRCommunicationBenchmarks, │
|
||||
│ SignalRRoundTripBenchmarks │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
▲
|
||||
│ ProjectReference (one-way)
|
||||
│
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ AyCode.Core.Serializers.Console │
|
||||
│ │
|
||||
│ BenchmarkLoop.cs custom adaptive measure engine │
|
||||
│ (CPU 0 pin, High priority, phase-isolated warmup, │
|
||||
│ 10-sample median + pilot, ~250ms/cell calibration) │
|
||||
│ Menu.cs / Configuration.cs / Program.cs Console UX │
|
||||
│ │
|
||||
│ Uses Benchmark's: │
|
||||
│ - Workloads/Scenarios/* (interface + concrete benchmarks) │
|
||||
│ - Reporting/BenchmarkReportWriter (SaveAll, Print...) │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Two runners — same workload, different measurement engines
|
||||
|
||||
| Aspect | Console (custom engine) | BDN |
|
||||
|---|---|---|
|
||||
| Use case | Fast iteration during micro-opt loops | Statistically confident before-commit validation |
|
||||
| Measurement | Adaptive per-cell iter (target ~250ms), 10 samples + pilot, median | Warmup + N iterations, outlier removal, JIT-stabilized, process-spawn isolation |
|
||||
| Time per full run | ~1-3 min | ~5-15 min |
|
||||
| Noise floor | ~3-5% inter-engine delta visible | ~1-2% |
|
||||
| Output format | Identical (same `BenchmarkReportWriter` writes both) | |
|
||||
|
||||
The Console and BDN outputs use the SAME `BenchmarkResult` DTO and the SAME formatter, so cells are directly comparable: pick a cell in `Console.FullBenchmark_*.LLM`, find the same cell in `Bdn.FullBenchmark_*.LLM` — deltas should agree within BDN's tighter CI.
|
||||
|
||||
## CLI
|
||||
|
||||
```
|
||||
dotnet run -c Release --project AyCode.Benchmark -- <switch>
|
||||
```
|
||||
|
||||
| Switch | Description |
|
||||
|---|---|
|
||||
| `--serializers` | AcBinary FastMode Byte[] vs MemoryPack Default Byte[] across 5 TestData cells (mirrors Console "F" menu / FastestByte). Emits `Bdn.FullBenchmark_*.{log,LLM,output}` + BDN-native artifacts under `BDN/`. |
|
||||
| `--jitasm` | JIT disassembly analysis (x64 asm of serialize/deserialize hot path). |
|
||||
| `--quick` | Quick inline benchmark (custom Stopwatch-based, not BDN). |
|
||||
| `--test` / `--testmsgpack` | Quick smoke tests. |
|
||||
| `--save-coverage <file>` | Save coverage file into `Test_Benchmark_Results/CoverageReport/`. |
|
||||
| _(no args)_ | Interactive `BenchmarkSwitcher` — pick from all `[Benchmark]` classes in the assembly. |
|
||||
|
||||
## Key files
|
||||
|
||||
### Serializer benchmark stack (the refactor scope)
|
||||
- [`AcBinaryVsMemPackBenchmark.cs`](AcBinaryVsMemPackBenchmark.cs) — BDN `[MemoryDiagnoser]` class. `[ParamsSource]`(TestData = Small/Medium/Large/Repeated/Deep) × `[Params]`(Engine = AcBinary/MemoryPack). `[GlobalSetup]` hidrátálja a Workloads scenario-ját + round-trip-verify.
|
||||
- [`BdnSummaryAdapter.cs`](BdnSummaryAdapter.cs) — `Summary → List<BenchmarkResult>` translator (groups per `(TestData × Engine)`, ns → ms conversion, GcStats → allocated-bytes-per-op). Calls `BenchmarkReportWriter.PrintGroupedResults` + `SaveAll(ctx with SourceTag="Bdn", ...)`.
|
||||
- [`Program.cs`](Program.cs) — BDN entry. Sets global `WithArtifactsPath(.../Benchmark/BDN)`; `--serializers` switch wires `BenchmarkRunner.Run<AcBinaryVsMemPackBenchmark>` + adapter.
|
||||
- [`Workloads/Scenarios/`](Workloads/Scenarios/) — shared workload types (see folder README).
|
||||
- [`Reporting/`](Reporting/) — shared reporting types (see folder README).
|
||||
|
||||
### KEEP benchmarks (independent — not in the serializer-refactor scope)
|
||||
- [`JitDisassemblyBenchmark.cs`](JitDisassemblyBenchmark.cs) — JIT analysis: emits `.asm` files for serialize/deserialize hot paths.
|
||||
- [`TaskHelperBenchmarks.cs`](TaskHelperBenchmarks.cs) — Task/timing utilities (WaitToAsync, custom ThreadPool, UtcNow.Ticks vs TickCount64).
|
||||
- [`ValueTypePassingBenchmark.cs`](ValueTypePassingBenchmark.cs) — Copy-by-value vs `in` parameter for 16-byte types.
|
||||
- [`RefForeachBenchmark.cs`](RefForeachBenchmark.cs) — Collection iteration patterns (array vs list, foreach vs index, ref readonly).
|
||||
- [`SourceGeneratorBenchmarks.cs`](SourceGeneratorBenchmarks.cs) — Source-generated vs runtime reflection serializers (PureContractlessBenchmark, RepeatedStringBenchmark).
|
||||
- [`SignalRCommunicationBenchmarks.cs`](SignalRCommunicationBenchmarks.cs) — Full-stack SignalR perf (client → server → response → round-trip).
|
||||
- [`SignalRRoundTripBenchmarks.cs`](SignalRRoundTripBenchmarks.cs) — SignalR primitives/complex/collections benchmarks.
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Dependency | Purpose |
|
||||
|---|---|
|
||||
| `BenchmarkDotNet` | Benchmarking framework |
|
||||
| `MessagePack` | Serialization comparison target |
|
||||
| `MongoDB.Bson` | BSON comparison target |
|
||||
| `BenchmarkDotNet` | BDN harness |
|
||||
| `MemoryPack` | Comparison target (used by Workloads scenarios + BDN class) |
|
||||
| `MessagePack` | Comparison target (KEEP benchmarks + Workloads MessagePackBenchmark scenario) |
|
||||
| `MongoDB.Bson` | KEEP-side comparison target |
|
||||
| `Microsoft.VisualStudio.DiagnosticsHub.BenchmarkDotNetDiagnosers` | VS Profiler integration |
|
||||
| `AyCode.Core` (ProjectReference) | AcBinary serializer |
|
||||
| `AyCode.Core.Tests` (ProjectReference) | Test data factory (`TestDataFactory`, `TestOrder_All_False/True`, `BenchmarkTestDataProvider*`) |
|
||||
| `AyCode.Core.Serializers.SourceGenerator` (Analyzer-only) | SGen for `[AcBinarySerializable]`-tagged types |
|
||||
|
|
|
|||
|
|
@ -57,6 +57,13 @@ public sealed class BenchmarkResult
|
|||
// (`SerializeTimeMs / SerializeIterations * 1000`). For round-trip-only rows (NamedPipe etc.),
|
||||
// RoundTripIterations carries the calibrated iter count; SerializeIterations and DeserializeIterations
|
||||
// stay 0 (Ser and Des are not separately measurable on those rows).
|
||||
//
|
||||
// BDN-sourced rows (populated by <c>BdnSummaryAdapter</c>) follow a different convention: per-op time
|
||||
// is stored directly in <c>*TimeMs</c> with <c>Iterations = 1</c>, so the same <c>TimeMs / Iterations * 1000</c>
|
||||
// formula yields per-op µs. The actual BDN N count is NOT preserved on these rows — consumers that
|
||||
// read <c>SerializeIterations</c> as a nominal loop count (e.g. "bytes allocated over N iterations")
|
||||
// will misinterpret BDN rows. For the raw N, read the BDN-native artifacts under
|
||||
// <c>Test_Benchmark_Results/Benchmark/BDN/</c>.
|
||||
public int SerializeIterations { get; set; }
|
||||
public int DeserializeIterations { get; set; }
|
||||
public int RoundTripIterations { get; set; }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
# Reporting
|
||||
|
||||
Shared reporting types — the `BenchmarkResult` DTO that captures one cell of a benchmark run + the `BenchmarkReportWriter` that turns a list of these into the unified `.log` / `.LLM` / `.output` triplet + the `ReportingContext` bundle that parameterises both runners.
|
||||
|
||||
## Layout
|
||||
|
||||
- [`BenchmarkResult.cs`](BenchmarkResult.cs) — per-cell result row. `(TestData × Engine × IoMode × OptionsPreset × DispatchMode)` tuple + Ser / Des / RT timings (median, min, max, stddev — all ms-batch units), iter counts (post-calibration), allocated bytes per op, setup-side one-time alloc, `IsRoundTripOnly` flag, derived `SerializerName`. Pure DTO — no behaviour. Populated by either:
|
||||
- Console `BenchmarkLoop.RunBenchmarksForTestData` (after adaptive measurement)
|
||||
- BDN `BdnSummaryAdapter.Translate` (after BDN Summary is in hand)
|
||||
|
||||
- [`ReportingContext.cs`](ReportingContext.cs) — record bundle for the writer:
|
||||
- `SourceTag` — `"Console"` / `"Bdn"`; drives the filename prefix
|
||||
- `ResultsDirectory` — resolved at startup via `ResolveResultsDirectory()` walking up from `AppContext.BaseDirectory` to the nearest `AyCode.Core.sln`, then `Test_Benchmark_Results/Benchmark/`. Worktree-aware.
|
||||
- `BuildConfiguration` — `"Debug"` / `"Release"` / `"NativeAOT"`; rendered into both the filename AND the report header
|
||||
- `Utf8NoBom` — shared `UTF8Encoding(false)` for all `File.WriteAllText` calls
|
||||
- `CharsetName`, `WarmupIterations`, `BenchmarkSamples`, `TargetSampleMs`, `UnstableCVThreshold` — run-header info embedded in every emitted artifact
|
||||
|
||||
- [`BenchmarkReportWriter.cs`](BenchmarkReportWriter.cs) — the writer itself:
|
||||
- `SaveAll(ctx, results, testDataSets)` — orchestrator. Writes the `.log` (formatted text + CSV + per-cell tables + Overall aggregation), `.LLM` (markdown table + Overall aggregation), and `.output` (hex dump of the Large cell's AcBinary serialization). All three land in `ctx.ResultsDirectory` with the `{ctx.SourceTag}.FullBenchmark_{Build}_{ts}.<ext>` filename pattern.
|
||||
- `PrintGroupedResults(results, testDataSets)` — colored per-cell tables to `System.Console`. Highlights MemoryPack (baseline) and AcBinary (SGen-Byte[]) rows with green/red win/lose colors, footer row shows pct deltas per metric.
|
||||
- `PrintResult(result)` — single-line summary printed during the per-cell loop (real-time progress signal).
|
||||
- `ComputeOverallStats(acResults, mpResults, valueSelector)` — paired-cell aggregation across `TestDataName` (arithmetic mean / geometric mean / median of per-cell ratios). Null-safe.
|
||||
- `FormatMicrosWithRange(...)` — `26.86 (24.50..29.10)` style with ⚠️CV-warning suffix when stddev/median exceeds the `UnstableCVThreshold`. All formatting goes through `CultureInfo.InvariantCulture` so the CSV section in `.log` stays parseable regardless of the host locale.
|
||||
- `ToPerOpMicros` / `SerPerOp` / `DesPerOp` / `RtPerOp` / `ToKilobytes` / `FormatPctSigned` / `FormatHexDump` / `AppendOverallLine` — helper utilities used inline by the report-rendering methods.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Time units in `BenchmarkResult`**: all `*TimeMs` fields are total-batch milliseconds. Per-op µs = `TimeMs / Iterations * 1000`. For BDN-sourced rows the adapter stores `Mean_ns / 1e6` with `Iterations = 1`, so the same formula yields per-op µs directly (`ms * 1000 = µs`).
|
||||
- **InvariantCulture** is enforced everywhere a numeric value is rendered to file (`.log` CSV section, `.LLM` markdown cells). Console-output (the colored tables) uses default culture for human-friendliness.
|
||||
- **`SourceTag` discriminator**: appears in three places — the filename prefix (`Console.` / `Bdn.`), the `.log` header (`║ Source: Console`), the `.LLM` H1 (`# AcBinary Benchmark [Console] Release ...`). Anyone diffing or grepping outputs can pin the source unambiguously.
|
||||
|
|
@ -1,713 +0,0 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using BenchmarkDotNet.Jobs;
|
||||
using MessagePack;
|
||||
using MessagePack.Resolvers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.IO;
|
||||
using MongoDB.Bson.Serialization;
|
||||
using System.IO;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
using AyCode.Core.Serializers;
|
||||
|
||||
namespace AyCode.Core.Benchmarks;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal benchmark to test if BenchmarkDotNet works without stack overflow.
|
||||
/// </summary>
|
||||
[ShortRunJob]
|
||||
[MemoryDiagnoser]
|
||||
public class MinimalBenchmark
|
||||
{
|
||||
private byte[] _data = null!;
|
||||
private string _json = null!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
// Use very simple data - no circular references
|
||||
var simpleData = new { Id = 1, Name = "Test", Value = 42.5 };
|
||||
_json = System.Text.Json.JsonSerializer.Serialize(simpleData);
|
||||
_data = Encoding.UTF8.GetBytes(_json);
|
||||
Console.WriteLine($"Setup complete. Data size: {_data.Length} bytes");
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int GetLength() => _data.Length;
|
||||
|
||||
[Benchmark]
|
||||
public string GetJson() => _json;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binary vs JSON benchmark with simple flat objects (no circular references).
|
||||
/// </summary>
|
||||
[ShortRunJob]
|
||||
[MemoryDiagnoser]
|
||||
public class SimpleBinaryBenchmark
|
||||
{
|
||||
private PrimitiveTestClass _testData = null!;
|
||||
private byte[] _binaryData = null!;
|
||||
private string _jsonData = null!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
_testData = TestDataFactory.CreatePrimitiveTestData();
|
||||
_binaryData = AcBinarySerializer.Serialize(_testData, AcBinarySerializerOptions.WithoutReferenceHandling);
|
||||
_jsonData = AcJsonSerializer.Serialize(_testData, AcJsonSerializerOptions.WithoutReferenceHandling);
|
||||
|
||||
Console.WriteLine($"Binary: {_binaryData.Length} bytes, JSON: {_jsonData.Length} chars");
|
||||
}
|
||||
|
||||
[Benchmark(Description = "Binary Serialize")]
|
||||
public byte[] SerializeBinary() => AcBinarySerializer.Serialize(_testData, AcBinarySerializerOptions.WithoutReferenceHandling);
|
||||
|
||||
[Benchmark(Description = "JSON Serialize", Baseline = true)]
|
||||
public string SerializeJson() => AcJsonSerializer.Serialize(_testData, AcJsonSerializerOptions.WithoutReferenceHandling);
|
||||
|
||||
[Benchmark(Description = "Binary Deserialize")]
|
||||
public PrimitiveTestClass? DeserializeBinary() => AcBinaryDeserializer.Deserialize<PrimitiveTestClass>(_binaryData);
|
||||
|
||||
[Benchmark(Description = "JSON Deserialize")]
|
||||
public PrimitiveTestClass? DeserializeJson() => AcJsonDeserializer.Deserialize<PrimitiveTestClass>(_jsonData, AcJsonSerializerOptions.WithoutReferenceHandling);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Complex hierarchy benchmark - AcBinary vs JSON only (no MessagePack to isolate the issue).
|
||||
/// Uses AcBinary without reference handling.
|
||||
/// </summary>
|
||||
[ShortRunJob]
|
||||
[MemoryDiagnoser]
|
||||
[RankColumn]
|
||||
public class ComplexBinaryBenchmark
|
||||
{
|
||||
private TestOrder_All_True _testOrder = null!;
|
||||
private byte[] _acBinaryData = null!;
|
||||
private string _jsonData = null!;
|
||||
|
||||
private AcBinarySerializerOptions _binaryOptions = null!;
|
||||
private AcJsonSerializerOptions _jsonOptions = null!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
Console.WriteLine("Creating test data...");
|
||||
_testOrder = TestDataFactory.CreateBenchmarkOrder(
|
||||
itemCount: 2,
|
||||
palletsPerItem: 2,
|
||||
measurementsPerPallet: 2,
|
||||
pointsPerMeasurement: 3);
|
||||
Console.WriteLine($"Created order with {_testOrder.Items.Count} items");
|
||||
|
||||
_binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
|
||||
_jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling;
|
||||
|
||||
Console.WriteLine("Serializing AcBinary...");
|
||||
_acBinaryData = AcBinarySerializer.Serialize(_testOrder, _binaryOptions);
|
||||
Console.WriteLine($"AcBinary size: {_acBinaryData.Length} bytes");
|
||||
|
||||
Console.WriteLine("Serializing JSON...");
|
||||
_jsonData = AcJsonSerializer.Serialize(_testOrder, _jsonOptions);
|
||||
Console.WriteLine($"JSON size: {_jsonData.Length} chars");
|
||||
|
||||
var jsonBytes = Encoding.UTF8.GetByteCount(_jsonData);
|
||||
Console.WriteLine($"\n=== SIZE COMPARISON ===");
|
||||
Console.WriteLine($"AcBinary: {_acBinaryData.Length,8:N0} bytes ({100.0 * _acBinaryData.Length / jsonBytes:F1}%)");
|
||||
Console.WriteLine($"JSON: {jsonBytes,8:N0} bytes (100.0%)");
|
||||
}
|
||||
|
||||
[Benchmark(Description = "AcBinary Serialize")]
|
||||
public byte[] Serialize_AcBinary() => AcBinarySerializer.Serialize(_testOrder, _binaryOptions);
|
||||
|
||||
[Benchmark(Description = "JSON Serialize", Baseline = true)]
|
||||
public string Serialize_Json() => AcJsonSerializer.Serialize(_testOrder, _jsonOptions);
|
||||
|
||||
[Benchmark(Description = "AcBinary Deserialize")]
|
||||
public TestOrder_All_True? Deserialize_AcBinary() => AcBinaryDeserializer.Deserialize<TestOrder_All_True>(_acBinaryData);
|
||||
|
||||
[Benchmark(Description = "JSON Deserialize")]
|
||||
public TestOrder_All_True? Deserialize_Json() => AcJsonDeserializer.Deserialize<TestOrder_All_True>(_jsonData, _jsonOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full comparison with MessagePack and BSON - AcBinary uses NO reference handling everywhere.
|
||||
/// </summary>
|
||||
[ShortRunJob]
|
||||
[MemoryDiagnoser]
|
||||
[RankColumn]
|
||||
public class MessagePackComparisonBenchmark
|
||||
{
|
||||
private TestOrder_All_True _testOrder = null!;
|
||||
private byte[] _acBinaryData = null!;
|
||||
private byte[] _msgPackData = null!;
|
||||
private byte[] _bsonData = null!;
|
||||
private string _jsonData = null!;
|
||||
|
||||
private AcBinarySerializerOptions _binaryOptions = null!;
|
||||
private MessagePackSerializerOptions _msgPackOptions = null!;
|
||||
private AcJsonSerializerOptions _jsonOptions = null!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
Console.WriteLine("Creating test data...");
|
||||
_testOrder = TestDataFactory.CreateBenchmarkOrder(
|
||||
itemCount: 2,
|
||||
palletsPerItem: 2,
|
||||
measurementsPerPallet: 2,
|
||||
pointsPerMeasurement: 3);
|
||||
|
||||
_binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
|
||||
_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
|
||||
_jsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling;
|
||||
|
||||
_acBinaryData = AcBinarySerializer.Serialize(_testOrder, _binaryOptions);
|
||||
_jsonData = AcJsonSerializer.Serialize(_testOrder, _jsonOptions);
|
||||
|
||||
// MessagePack serialization in try-catch to see if it fails
|
||||
try
|
||||
{
|
||||
Console.WriteLine("Serializing MessagePack...");
|
||||
_msgPackData = MessagePackSerializer.Serialize(_testOrder, _msgPackOptions);
|
||||
Console.WriteLine($"MessagePack size: {_msgPackData.Length} bytes");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"MessagePack serialization failed: {ex.Message}");
|
||||
_msgPackData = Array.Empty<byte>();
|
||||
}
|
||||
|
||||
// BSON serialization
|
||||
try
|
||||
{
|
||||
Console.WriteLine("Serializing BSON...");
|
||||
var bsonDoc = _testOrder.ToBsonDocument();
|
||||
_bsonData = bsonDoc.ToBson();
|
||||
Console.WriteLine($"BSON size: {_bsonData.Length} bytes");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"BSON serialization failed: {ex.Message}");
|
||||
_bsonData = Array.Empty<byte>();
|
||||
}
|
||||
|
||||
var jsonBytes = Encoding.UTF8.GetByteCount(_jsonData);
|
||||
Console.WriteLine($"\n=== SIZE COMPARISON ===");
|
||||
Console.WriteLine($"AcBinary: {_acBinaryData.Length,8:N0} bytes ({100.0 * _acBinaryData.Length / jsonBytes:F1}%)");
|
||||
Console.WriteLine($"MessagePack: {_msgPackData.Length,8:N0} bytes ({100.0 * _msgPackData.Length / jsonBytes:F1}%)");
|
||||
Console.WriteLine($"BSON: {_bsonData.Length,8:N0} bytes ({100.0 * _bsonData.Length / jsonBytes:F1}%)");
|
||||
Console.WriteLine($"JSON: {jsonBytes,8:N0} bytes (100.0%)");
|
||||
}
|
||||
|
||||
[Benchmark(Description = "AcBinary Serialize")]
|
||||
public byte[] Serialize_AcBinary() => AcBinarySerializer.Serialize(_testOrder, _binaryOptions);
|
||||
|
||||
[Benchmark(Description = "MessagePack Serialize", Baseline = true)]
|
||||
public byte[] Serialize_MsgPack() => MessagePackSerializer.Serialize(_testOrder, _msgPackOptions);
|
||||
|
||||
[Benchmark(Description = "BSON Serialize")]
|
||||
public byte[] Serialize_Bson() => _testOrder.ToBsonDocument().ToBson();
|
||||
|
||||
[Benchmark(Description = "AcBinary Deserialize")]
|
||||
public TestOrder_All_True? Deserialize_AcBinary() => AcBinaryDeserializer.Deserialize<TestOrder_All_True>(_acBinaryData);
|
||||
|
||||
[Benchmark(Description = "MessagePack Deserialize")]
|
||||
public TestOrder_All_True? Deserialize_MsgPack() => MessagePackSerializer.Deserialize<TestOrder_All_True>(_msgPackData, _msgPackOptions);
|
||||
|
||||
[Benchmark(Description = "BSON Deserialize")]
|
||||
public TestOrder_All_True? Deserialize_Bson()
|
||||
{
|
||||
if (_bsonData == null || _bsonData.Length == 0) return null;
|
||||
using var ms = new MemoryStream(_bsonData);
|
||||
using var reader = new BsonBinaryReader(ms);
|
||||
return BsonSerializer.Deserialize<TestOrder_All_True>(reader);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Comprehensive AcBinary vs MessagePack comparison benchmark.
|
||||
/// Tests: NoRef (everywhere), Populate, Serialize, Deserialize, Size
|
||||
/// </summary>
|
||||
[ShortRunJob]
|
||||
[MemoryDiagnoser]
|
||||
[RankColumn]
|
||||
public class AcBinaryVsMessagePackFullBenchmark
|
||||
{
|
||||
// Test data
|
||||
private TestOrder_All_True _testOrder = null!;
|
||||
private TestOrder_All_True _populateTarget = null!;
|
||||
|
||||
// Serialized data - AcBinary
|
||||
private byte[] _acBinaryWithRef = null!;
|
||||
private byte[] _acBinaryNoRef = null!;
|
||||
|
||||
// Serialized data - MessagePack
|
||||
private byte[] _msgPackData = null!;
|
||||
private byte[] _bsonData = null!;
|
||||
|
||||
// Options
|
||||
private AcBinarySerializerOptions _withRefOptions = null!;
|
||||
private AcBinarySerializerOptions _noRefOptions = null!;
|
||||
private MessagePackSerializerOptions _msgPackOptions = null!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
// Create test data with shared references
|
||||
TestDataFactory.ResetIdCounter();
|
||||
var sharedTag = TestDataFactory.CreateTag("SharedTag_All_True");
|
||||
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
||||
var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);
|
||||
|
||||
_testOrder = TestDataFactory.CreateOrder(
|
||||
itemCount: 3,
|
||||
palletsPerItem: 3,
|
||||
measurementsPerPallet: 3,
|
||||
pointsPerMeasurement: 4,
|
||||
sharedTag: sharedTag,
|
||||
sharedUser: sharedUser,
|
||||
sharedMetadata: sharedMeta);
|
||||
|
||||
// Setup options - WithRef uses Default (which has reference handling), NoRef explicitly disables it
|
||||
_withRefOptions = AcBinarySerializerOptions.Default;
|
||||
_noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
|
||||
_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
|
||||
|
||||
// Serialize with different options
|
||||
_acBinaryWithRef = AcBinarySerializer.Serialize(_testOrder, _withRefOptions);
|
||||
_acBinaryNoRef = AcBinarySerializer.Serialize(_testOrder, _noRefOptions);
|
||||
_msgPackData = MessagePackSerializer.Serialize(_testOrder, _msgPackOptions);
|
||||
|
||||
// BSON
|
||||
try
|
||||
{
|
||||
_bsonData = _testOrder.ToBsonDocument().ToBson();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_bsonData = Array.Empty<byte>();
|
||||
}
|
||||
|
||||
// Create populate target
|
||||
_populateTarget = new TestOrder_All_True { Id = _testOrder.Id };
|
||||
foreach (var item in _testOrder.Items)
|
||||
{
|
||||
_populateTarget.Items.Add(new TestOrderItem_All_True { Id = item.Id });
|
||||
}
|
||||
|
||||
// Print size comparison
|
||||
PrintSizeComparison();
|
||||
}
|
||||
|
||||
private void PrintSizeComparison()
|
||||
{
|
||||
Console.WriteLine("\n" + new string('=', 60));
|
||||
Console.WriteLine("?? SIZE COMPARISON (AcBinary vs MessagePack vs BSON)");
|
||||
Console.WriteLine(new string('=', 60));
|
||||
Console.WriteLine($" AcBinary WithRef: {_acBinaryWithRef.Length,8:N0} bytes");
|
||||
Console.WriteLine($" AcBinary NoRef: {_acBinaryNoRef.Length,8:N0} bytes");
|
||||
Console.WriteLine($" MessagePack: {_msgPackData.Length,8:N0} bytes");
|
||||
Console.WriteLine($" BSON: {_bsonData.Length,8:N0} bytes");
|
||||
Console.WriteLine(new string('-', 60));
|
||||
Console.WriteLine($" AcBinary/MsgPack: {100.0 * _acBinaryWithRef.Length / Math.Max(1, _msgPackData.Length):F1}% (WithRef)");
|
||||
Console.WriteLine($" AcBinary/MsgPack: {100.0 * _acBinaryNoRef.Length / Math.Max(1, _msgPackData.Length):F1}% (NoRef)");
|
||||
Console.WriteLine(new string('=', 60) + "\n");
|
||||
}
|
||||
|
||||
#region Serialize Benchmarks
|
||||
|
||||
[Benchmark(Description = "AcBinary Serialize WithRef")]
|
||||
public byte[] Serialize_AcBinary_WithRef() => AcBinarySerializer.Serialize(_testOrder, _withRefOptions);
|
||||
|
||||
[Benchmark(Description = "AcBinary Serialize NoRef")]
|
||||
public byte[] Serialize_AcBinary_NoRef() => AcBinarySerializer.Serialize(_testOrder, _noRefOptions);
|
||||
|
||||
[Benchmark(Description = "MessagePack Serialize", Baseline = true)]
|
||||
public byte[] Serialize_MsgPack() => MessagePackSerializer.Serialize(_testOrder, _msgPackOptions);
|
||||
|
||||
[Benchmark(Description = "BSON Serialize")]
|
||||
public byte[] Serialize_Bson() => _testOrder.ToBsonDocument().ToBson();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Deserialize Benchmarks
|
||||
|
||||
[Benchmark(Description = "AcBinary Deserialize WithRef")]
|
||||
public TestOrder_All_True? Deserialize_AcBinary_WithRef() => AcBinaryDeserializer.Deserialize<TestOrder_All_True>(_acBinaryWithRef);
|
||||
|
||||
[Benchmark(Description = "AcBinary Deserialize NoRef")]
|
||||
public TestOrder_All_True? Deserialize_AcBinary_NoRef() => AcBinaryDeserializer.Deserialize<TestOrder_All_True>(_acBinaryNoRef);
|
||||
|
||||
[Benchmark(Description = "MessagePack Deserialize")]
|
||||
public TestOrder_All_True? Deserialize_MsgPack() => MessagePackSerializer.Deserialize<TestOrder_All_True>(_msgPackData, _msgPackOptions);
|
||||
|
||||
[Benchmark(Description = "BSON Deserialize")]
|
||||
public TestOrder_All_True? Deserialize_Bson()
|
||||
{
|
||||
if (_bsonData == null || _bsonData.Length == 0) return null;
|
||||
using var ms = new MemoryStream(_bsonData);
|
||||
using var reader = new BsonBinaryReader(ms);
|
||||
return BsonSerializer.Deserialize<TestOrder_All_True>(reader);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Populate Benchmarks
|
||||
|
||||
[Benchmark(Description = "AcBinary Populate WithRef")]
|
||||
public void Populate_AcBinary_WithRef()
|
||||
{
|
||||
var target = CreatePopulateTarget();
|
||||
AcBinaryDeserializer.Populate(_acBinaryWithRef, target);
|
||||
}
|
||||
|
||||
[Benchmark(Description = "AcBinary Populate NoRef")]
|
||||
public void Populate_AcBinary_NoRef()
|
||||
{
|
||||
// Create fresh target each time to avoid state accumulation
|
||||
var target = CreatePopulateTarget();
|
||||
AcBinaryDeserializer.Populate(_acBinaryNoRef, target);
|
||||
}
|
||||
|
||||
[Benchmark(Description = "AcBinary PopulateMerge WithRef")]
|
||||
public void PopulateMerge_AcBinary_WithRef()
|
||||
{
|
||||
var target = CreatePopulateTarget();
|
||||
AcBinaryDeserializer.PopulateMerge(_acBinaryWithRef, target);
|
||||
}
|
||||
|
||||
[Benchmark(Description = "AcBinary PopulateMerge NoRef")]
|
||||
public void PopulateMerge_AcBinary_NoRef()
|
||||
{
|
||||
// Create fresh target each time to avoid state accumulation
|
||||
var target = CreatePopulateTarget();
|
||||
AcBinaryDeserializer.PopulateMerge(_acBinaryNoRef, target);
|
||||
}
|
||||
|
||||
private TestOrder_All_True CreatePopulateTarget()
|
||||
{
|
||||
var target = new TestOrder_All_True { Id = _testOrder.Id };
|
||||
foreach (var item in _testOrder.Items)
|
||||
{
|
||||
target.Items.Add(new TestOrderItem_All_True { Id = item.Id });
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detailed size comparison - not a performance benchmark, just size output.
|
||||
/// Now includes BSON size output and uses AcBinary without reference handling.
|
||||
/// </summary>
|
||||
[ShortRunJob]
|
||||
[MemoryDiagnoser]
|
||||
public class SizeComparisonBenchmark
|
||||
{
|
||||
private TestOrder_All_True _smallOrder = null!;
|
||||
private TestOrder_All_True _mediumOrder = null!;
|
||||
private TestOrder_All_True _largeOrder = null!;
|
||||
|
||||
private MessagePackSerializerOptions _msgPackOptions = null!;
|
||||
private AcBinarySerializerOptions _withRefOptions = null!;
|
||||
private AcBinarySerializerOptions _noRefOptions = null!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
|
||||
_withRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
|
||||
_noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
|
||||
|
||||
// Small order
|
||||
TestDataFactory.ResetIdCounter();
|
||||
_smallOrder = TestDataFactory.CreateOrder(itemCount: 1, palletsPerItem: 1, measurementsPerPallet: 1, pointsPerMeasurement: 2);
|
||||
|
||||
// Medium order
|
||||
TestDataFactory.ResetIdCounter();
|
||||
var sharedTag = TestDataFactory.CreateTag("Shared");
|
||||
var sharedUser = TestDataFactory.CreateUser("shared");
|
||||
_mediumOrder = TestDataFactory.CreateOrder(
|
||||
itemCount: 3, palletsPerItem: 2, measurementsPerPallet: 2, pointsPerMeasurement: 3,
|
||||
sharedTag: sharedTag, sharedUser: sharedUser);
|
||||
|
||||
// Large order
|
||||
TestDataFactory.ResetIdCounter();
|
||||
sharedTag = TestDataFactory.CreateTag("SharedLarge");
|
||||
sharedUser = TestDataFactory.CreateUser("sharedlarge");
|
||||
var sharedMeta = TestDataFactory.CreateMetadata("meta", withChild: true);
|
||||
_largeOrder = TestDataFactory.CreateOrder(
|
||||
itemCount: 5, palletsPerItem: 4, measurementsPerPallet: 3, pointsPerMeasurement: 5,
|
||||
sharedTag: sharedTag, sharedUser: sharedUser, sharedMetadata: sharedMeta);
|
||||
|
||||
PrintDetailedSizeComparison();
|
||||
}
|
||||
|
||||
private void PrintDetailedSizeComparison()
|
||||
{
|
||||
Console.WriteLine("\n" + new string('=', 80));
|
||||
Console.WriteLine("?? DETAILED SIZE COMPARISON: AcBinary vs MessagePack vs BSON");
|
||||
Console.WriteLine(new string('=', 80));
|
||||
|
||||
PrintOrderSize("Small Order (1x1x1x2)", _smallOrder);
|
||||
PrintOrderSize("Medium Order (3x2x2x3) + SharedRefs", _mediumOrder);
|
||||
PrintOrderSize("Large Order (5x4x3x5) + SharedRefs", _largeOrder);
|
||||
|
||||
Console.WriteLine(new string('=', 80) + "\n");
|
||||
}
|
||||
|
||||
private void PrintOrderSize(string name, TestOrder_All_True order)
|
||||
{
|
||||
var acWithRef = AcBinarySerializer.Serialize(order, _withRefOptions);
|
||||
var acNoRef = AcBinarySerializer.Serialize(order, _noRefOptions);
|
||||
var msgPack = MessagePackSerializer.Serialize(order, _msgPackOptions);
|
||||
byte[] bson;
|
||||
try { bson = order.ToBsonDocument().ToBson(); } catch { bson = Array.Empty<byte>(); }
|
||||
|
||||
Console.WriteLine($"\n {name}:");
|
||||
Console.WriteLine($" AcBinary WithRef: {acWithRef.Length,8:N0} bytes ({100.0 * acWithRef.Length / Math.Max(1, msgPack.Length),5:F1}% of MsgPack)");
|
||||
Console.WriteLine($" AcBinary NoRef: {acNoRef.Length,8:N0} bytes ({100.0 * acNoRef.Length / Math.Max(1, msgPack.Length),5:F1}% of MsgPack)");
|
||||
Console.WriteLine($" MessagePack: {msgPack.Length,8:N0} bytes (100.0%)");
|
||||
Console.WriteLine($" BSON: {bson.Length,8:N0} bytes (compared to MsgPack)");
|
||||
|
||||
var withRefSaving = msgPack.Length - acWithRef.Length;
|
||||
var noRefSaving = msgPack.Length - acNoRef.Length;
|
||||
if (withRefSaving > 0)
|
||||
Console.WriteLine($" ?? AcBinary WithRef saves: {withRefSaving:N0} bytes ({100.0 * withRefSaving / msgPack.Length:F1}%)");
|
||||
else
|
||||
Console.WriteLine($" ?? AcBinary WithRef larger by: {-withRefSaving:N0} bytes");
|
||||
}
|
||||
|
||||
[Benchmark(Description = "Placeholder")]
|
||||
public int Placeholder() => 1; // Just to make BenchmarkDotNet happy
|
||||
}
|
||||
|
||||
public enum BinaryBenchmarkMode
|
||||
{
|
||||
Default,
|
||||
NoReferenceHandling,
|
||||
FastMode
|
||||
}
|
||||
|
||||
public abstract class AcBinaryOptionsBenchmarkBase
|
||||
{
|
||||
protected TestOrder_All_True TestOrder = null!;
|
||||
protected AcBinarySerializerOptions BinaryOptions = null!;
|
||||
protected MessagePackSerializerOptions MsgPackOptions = null!;
|
||||
protected byte[] AcBinaryData = null!;
|
||||
protected byte[] MsgPackData = null!;
|
||||
|
||||
[Params(BinaryBenchmarkMode.Default, BinaryBenchmarkMode.NoReferenceHandling, BinaryBenchmarkMode.FastMode)]
|
||||
public BinaryBenchmarkMode Mode { get; set; }
|
||||
|
||||
[GlobalSetup]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
TestDataFactory.ResetIdCounter();
|
||||
TestOrder = TestDataFactory.CreateBenchmarkOrder(
|
||||
itemCount: 4,
|
||||
palletsPerItem: 3,
|
||||
measurementsPerPallet: 3,
|
||||
pointsPerMeasurement: 6);
|
||||
|
||||
BinaryOptions = CreateBinaryOptions(Mode);
|
||||
MsgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
|
||||
|
||||
AcBinaryData = AcBinarySerializer.Serialize(TestOrder, BinaryOptions);
|
||||
MsgPackData = MessagePackSerializer.Serialize(TestOrder, MsgPackOptions);
|
||||
|
||||
var ratio = MsgPackData.Length == 0 ? 0 : 100.0 * AcBinaryData.Length / MsgPackData.Length;
|
||||
Console.WriteLine($"[BenchmarkSetup] Mode={Mode} | AcBinary={AcBinaryData.Length} bytes | MessagePack={MsgPackData.Length} bytes | Ratio={ratio:F1}%");
|
||||
}
|
||||
|
||||
private static AcBinarySerializerOptions CreateBinaryOptions(BinaryBenchmarkMode mode) => mode switch
|
||||
{
|
||||
BinaryBenchmarkMode.Default => new AcBinarySerializerOptions(),
|
||||
BinaryBenchmarkMode.NoReferenceHandling => AcBinarySerializerOptions.WithoutReferenceHandling,
|
||||
BinaryBenchmarkMode.FastMode => new AcBinarySerializerOptions
|
||||
{
|
||||
UseMetadata = false,
|
||||
UseStringInterning = StringInterningMode.None,
|
||||
ReferenceHandling = ReferenceHandlingMode.None,
|
||||
},
|
||||
_ => new AcBinarySerializerOptions()
|
||||
};
|
||||
}
|
||||
|
||||
[ShortRunJob]
|
||||
[MemoryDiagnoser]
|
||||
[RankColumn]
|
||||
public class AcBinaryOptionsSerializeBenchmark : AcBinaryOptionsBenchmarkBase
|
||||
{
|
||||
[Benchmark(Description = "MessagePack Serialize", Baseline = true)]
|
||||
public byte[] Serialize_MessagePack() => MessagePackSerializer.Serialize(TestOrder, MsgPackOptions);
|
||||
|
||||
[Benchmark(Description = "AcBinary Serialize")]
|
||||
public byte[] Serialize_AcBinary() => AcBinarySerializer.Serialize(TestOrder, BinaryOptions);
|
||||
}
|
||||
|
||||
[ShortRunJob]
|
||||
[MemoryDiagnoser]
|
||||
[RankColumn]
|
||||
public class AcBinaryOptionsDeserializeBenchmark : AcBinaryOptionsBenchmarkBase
|
||||
{
|
||||
[Benchmark(Description = "MessagePack Deserialize", Baseline = true)]
|
||||
public TestOrder_All_True? Deserialize_MessagePack() => MessagePackSerializer.Deserialize<TestOrder_All_True>(MsgPackData, MsgPackOptions);
|
||||
|
||||
[Benchmark(Description = "AcBinary Deserialize")]
|
||||
public TestOrder_All_True? Deserialize_AcBinary() => AcBinaryDeserializer.Deserialize<TestOrder_All_True>(AcBinaryData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Large-scale benchmark simulating production workloads.
|
||||
/// Tests with ~50,000+ IId objects with deep hierarchy and shared references.
|
||||
/// This is closer to real-world scenarios with 2200 root items and 4-5MB binary data.
|
||||
/// </summary>
|
||||
[ShortRunJob]
|
||||
[MemoryDiagnoser]
|
||||
[RankColumn]
|
||||
public class LargeScaleBinaryBenchmark
|
||||
{
|
||||
// Test data - smaller scale for benchmark (500 items ? 25K objects)
|
||||
// Production would be 2200 items ? 100K+ objects
|
||||
private TestOrder_All_True _testOrder = null!;
|
||||
private TestOrder_All_True _populateTarget = null!;
|
||||
|
||||
// Serialized data
|
||||
private byte[] _acBinaryData = null!;
|
||||
private byte[] _msgPackData = null!;
|
||||
|
||||
// Options
|
||||
private AcBinarySerializerOptions _binaryOptions = null!;
|
||||
private MessagePackSerializerOptions _msgPackOptions = null!;
|
||||
|
||||
private int _objectCount;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
Console.WriteLine("Creating large-scale test data...");
|
||||
|
||||
// Use 500 root items for benchmark (?25K objects)
|
||||
// Production would use 2200 (?100K+ objects)
|
||||
const int rootItems = 500;
|
||||
const int pallets = 3;
|
||||
const int measurements = 3;
|
||||
const int points = 4;
|
||||
|
||||
_objectCount = TestDataFactory.CalculateObjectCount(rootItems, pallets, measurements, points);
|
||||
Console.WriteLine($"Creating ~{_objectCount:N0} IId objects...");
|
||||
|
||||
_testOrder = TestDataFactory.CreateLargeScaleBenchmarkOrder(rootItems, pallets, measurements, points);
|
||||
Console.WriteLine($"Created order with {_testOrder.Items.Count} root items");
|
||||
|
||||
_binaryOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
|
||||
_msgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
|
||||
|
||||
Console.WriteLine("Serializing AcBinary...");
|
||||
_acBinaryData = AcBinarySerializer.Serialize(_testOrder, _binaryOptions);
|
||||
|
||||
Console.WriteLine("Serializing MessagePack...");
|
||||
_msgPackData = MessagePackSerializer.Serialize(_testOrder, _msgPackOptions);
|
||||
|
||||
// Create populate target
|
||||
_populateTarget = new TestOrder_All_True { Id = _testOrder.Id };
|
||||
foreach (var item in _testOrder.Items.Take(10)) // Only first 10 for populate target
|
||||
{
|
||||
_populateTarget.Items.Add(new TestOrderItem_All_True { Id = item.Id });
|
||||
}
|
||||
|
||||
PrintStats();
|
||||
}
|
||||
|
||||
private void PrintStats()
|
||||
{
|
||||
Console.WriteLine("\n" + new string('=', 70));
|
||||
Console.WriteLine("?? LARGE-SCALE BENCHMARK STATS");
|
||||
Console.WriteLine(new string('=', 70));
|
||||
Console.WriteLine($" Root Items: {_testOrder.Items.Count:N0}");
|
||||
Console.WriteLine($" Total Objects: ~{_objectCount:N0} IId objects");
|
||||
Console.WriteLine($" AcBinary Size: {_acBinaryData.Length:N0} bytes ({_acBinaryData.Length / 1024.0 / 1024.0:F2} MB)");
|
||||
Console.WriteLine($" MsgPack Size: {_msgPackData.Length:N0} bytes ({_msgPackData.Length / 1024.0 / 1024.0:F2} MB)");
|
||||
Console.WriteLine($" Size Ratio: {100.0 * _acBinaryData.Length / _msgPackData.Length:F1}% of MsgPack");
|
||||
Console.WriteLine(new string('=', 70) + "\n");
|
||||
}
|
||||
|
||||
[Benchmark(Description = "LargeScale AcBinary Deserialize")]
|
||||
public TestOrder_All_True? Deserialize_AcBinary() => AcBinaryDeserializer.Deserialize<TestOrder_All_True>(_acBinaryData);
|
||||
|
||||
[Benchmark(Description = "LargeScale MsgPack Deserialize", Baseline = true)]
|
||||
public TestOrder_All_True? Deserialize_MsgPack() => MessagePackSerializer.Deserialize<TestOrder_All_True>(_msgPackData, _msgPackOptions);
|
||||
|
||||
[Benchmark(Description = "LargeScale AcBinary Serialize")]
|
||||
public byte[] Serialize_AcBinary() => AcBinarySerializer.Serialize(_testOrder, _binaryOptions);
|
||||
|
||||
[Benchmark(Description = "LargeScale MsgPack Serialize")]
|
||||
public byte[] Serialize_MsgPack() => MessagePackSerializer.Serialize(_testOrder, _msgPackOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AcJson vs System.Text.Json comparison - measures Newtonsoft.Json based AcJson against modern STJ.
|
||||
/// Uses simple flat object (PrimitiveTestClass) to avoid circular reference issues.
|
||||
/// </summary>
|
||||
[ShortRunJob]
|
||||
[MemoryDiagnoser]
|
||||
[RankColumn]
|
||||
public class AcJsonVsSystemTextJsonBenchmark
|
||||
{
|
||||
private PrimitiveTestClass _testData = null!;
|
||||
private string _acJsonData = null!;
|
||||
private string _stjData = null!;
|
||||
private AcJsonSerializerOptions _acJsonOptions = null!;
|
||||
private JsonSerializerOptions _stjOptions = null!;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
Console.WriteLine("Creating test data for AcJson vs System.Text.Json...");
|
||||
|
||||
// Use simple flat object to avoid circular reference issues
|
||||
_testData = TestDataFactory.CreatePrimitiveTestData();
|
||||
|
||||
// Setup options
|
||||
_acJsonOptions = AcJsonSerializerOptions.WithoutReferenceHandling;
|
||||
_stjOptions = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||
ReferenceHandler = null // No reference handling
|
||||
};
|
||||
|
||||
// Pre-serialize
|
||||
_acJsonData = AcJsonSerializer.Serialize(_testData, _acJsonOptions);
|
||||
_stjData = JsonSerializer.Serialize(_testData, _stjOptions);
|
||||
|
||||
Console.WriteLine($"AcJson size: {_acJsonData.Length:N0} chars");
|
||||
Console.WriteLine($"STJ size: {_stjData.Length:N0} chars");
|
||||
Console.WriteLine($"Size ratio: {100.0 * _acJsonData.Length / _stjData.Length:F1}%");
|
||||
}
|
||||
|
||||
[Benchmark(Description = "AcJson Serialize", Baseline = true)]
|
||||
public string Serialize_AcJson() =>
|
||||
AcJsonSerializer.Serialize(_testData, _acJsonOptions);
|
||||
|
||||
[Benchmark(Description = "System.Text.Json Serialize")]
|
||||
public string Serialize_STJ() =>
|
||||
JsonSerializer.Serialize(_testData, _stjOptions);
|
||||
|
||||
[Benchmark(Description = "AcJson Deserialize")]
|
||||
public PrimitiveTestClass? Deserialize_AcJson() =>
|
||||
AcJsonDeserializer.Deserialize<PrimitiveTestClass>(_acJsonData, _acJsonOptions);
|
||||
|
||||
[Benchmark(Description = "System.Text.Json Deserialize")]
|
||||
public PrimitiveTestClass? Deserialize_STJ() =>
|
||||
JsonSerializer.Deserialize<PrimitiveTestClass>(_stjData, _stjOptions);
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# Workloads / Scenarios
|
||||
|
||||
Shared workload + scenario types used by **both** the Console runner (custom adaptive measure engine) and the BDN runner (`AcBinaryVsMemPackBenchmark` in the parent folder). Same wire payloads, same options, same round-trip-verify gate → Console and BDN cells are directly comparable.
|
||||
|
||||
## Layout
|
||||
|
||||
### Contract types
|
||||
|
||||
- [`ISerializerBenchmark.cs`](ISerializerBenchmark.cs) — common contract for every (Engine × IoMode × OptionsPreset) row. `Serialize()` / `Deserialize()` hot-path + warmup hooks + `VerifyRoundTrip()` for the pre-warmup correctness gate. Round-trip-only benchmarks (NamedPipe / in-memory Pipe) set `IsRoundTripOnly = true` and let the bench loop skip the Des-phase.
|
||||
- [`BenchmarkEnums.cs`](BenchmarkEnums.cs) — `BenchmarkEngine` / `BenchmarkIoMode` / `BenchmarkDispatchMode` / `BenchmarkLayer` / `BenchmarkOpMode` / `SerializerSelectionMode` + `ToDisplay()` extensions for the column-friendly rendering used by every output formatter.
|
||||
- [`BenchmarkOptions.cs`](BenchmarkOptions.cs) — per-engine options-formatting helpers + the cached `AttrFlags` aggregation (assembly-scan of `[AcBinarySerializable]` feature flags) + `GetMemPack(WireMode)` for the wire-mode-aligned MemoryPack-options selection.
|
||||
- [`RoundTripValidator.cs`](RoundTripValidator.cs) — universal deep-equality oracle via canonical System.Text.Json. Called by every benchmark's `VerifyRoundTrip()` before warmup. AOT-skipped (STJ reflection path incompatible).
|
||||
|
||||
### Concrete benchmarks (12 implementations)
|
||||
|
||||
**AcBinary** (7 variants — different I/O modes):
|
||||
- [`AcBinaryBenchmark.cs`](AcBinaryBenchmark.cs) — `Byte[]` API. Headline AcBinary row.
|
||||
- [`AcBinaryBufferWriterBenchmark.cs`](AcBinaryBufferWriterBenchmark.cs) — pre-allocated, reused `ArrayBufferWriter<byte>`.
|
||||
- [`AcBinaryFreshBufferWriterBenchmark.cs`](AcBinaryFreshBufferWriterBenchmark.cs) — fresh `ArrayBufferWriter` per call (one-shot scenario, 4 KB chunk).
|
||||
- [`AcBinaryNamedPipeBenchmark.cs`](AcBinaryNamedPipeBenchmark.cs) — chunked-framed `AsyncPipe` over kernel NamedPipe (long-lived, multi-message, 2-task pipeline).
|
||||
- [`AcBinaryNamedPipeRawByteArrayBenchmark.cs`](AcBinaryNamedPipeRawByteArrayBenchmark.cs) — raw `byte[]` over kernel NamedPipe (no chunk-framing, Read+Des sequential after Read completes).
|
||||
- [`AcBinaryInMemoryPipeBenchmark.cs`](AcBinaryInMemoryPipeBenchmark.cs) — chunked-framed `AsyncPipe` over in-memory `System.IO.Pipelines.Pipe` (zero kernel involvement, isolates streaming-framework CPU cost from kernel-pipe transport overhead).
|
||||
- [`AcBinaryInMemoryRawByteArrayBenchmark.cs`](AcBinaryInMemoryRawByteArrayBenchmark.cs) — raw `byte[]` over in-memory cross-thread handoff (no transport at all, completes the 2×2 [chunked|raw] × [kernel|memory] matrix).
|
||||
|
||||
**MemoryPack** (3 variants — apples-to-apples with the AcBinary I/O modes):
|
||||
- [`MemoryPackBenchmark.cs`](MemoryPackBenchmark.cs) — `Byte[]` API. SOTA baseline.
|
||||
- [`MemoryPackBufferWriterBenchmark.cs`](MemoryPackBufferWriterBenchmark.cs) — reused `ArrayBufferWriter`.
|
||||
- [`MemoryPackFreshBufferWriterBenchmark.cs`](MemoryPackFreshBufferWriterBenchmark.cs) — fresh `ArrayBufferWriter` per call.
|
||||
|
||||
**Other** (reference comparison, typically disabled in active suite):
|
||||
- [`MessagePackBenchmark.cs`](MessagePackBenchmark.cs) — JIT-only (AOT-incompatible — v3 StandardResolver falls back to `Activator.CreateInstance` on trimmed closed-generic types).
|
||||
- [`SystemTextJsonBenchmark.cs`](SystemTextJsonBenchmark.cs) — String I/O mode, reflection-based metadata. Far behind binary serializers on µs/op; useful as a JSON baseline when activated.
|
||||
|
||||
## Convention
|
||||
|
||||
Every concrete benchmark:
|
||||
|
||||
1. Stores the test data graph + serializer options in its ctor and pre-computes a `_serialized` byte array for `SerializedSize` reporting.
|
||||
2. Implements `Serialize()` / `Deserialize()` as `[MethodImpl(NoInlining)]` hot-paths — the bench loop drives these directly through warmup + adaptive-iter calibration + measurement.
|
||||
3. Implements `VerifyRoundTrip()` by calling `RoundTripValidator.DeepEqualsViaJson(original, roundTripped)` on the result of a single Ser+Des pass.
|
||||
4. Round-trip-only variants (NamedPipe / in-memory Pipe) override `IsRoundTripOnly => true`, route the full Ser+wire+Des roundtrip through `Serialize()`, and leave `Deserialize()` as a no-op.
|
||||
|
||||
The runner (Console `BenchmarkLoop` or BDN `AcBinaryVsMemPackBenchmark`) creates the appropriate concrete via factory helpers and drives the contract — no scenario-specific knowledge in the runner.
|
||||
|
|
@ -413,8 +413,9 @@ internal static class BenchmarkLoop
|
|||
{
|
||||
MakeAcBinary(testData, fastestByteOptions, "FastMode"),
|
||||
//MakeAcBinary(testData, fastWireOptions, "FastMode (FastWire)"),
|
||||
// MemPack canonically on _All_True (no AcBinary opt-in/opt-out axis applies; the MemoryPackable
|
||||
// contract serialises identical bytes either way, but _All_True is the established baseline).
|
||||
// MemPack uses _All_False (the AcBinary opt-in/opt-out axis doesn't apply — MemoryPackable
|
||||
// serialises identical bytes either way; _All_False matches the orderFalse variant the test
|
||||
// data factory already built, no extra graph allocation needed).
|
||||
new MemoryPackBenchmark<TestOrder_All_False>(orderFalse, Configuration.SelectedWireMode, "Default"),
|
||||
};
|
||||
}
|
||||
|
|
@ -552,7 +553,7 @@ internal static class BenchmarkLoop
|
|||
// ============================================================
|
||||
// MemoryPack — three I/O modes for apples-to-apples comparison
|
||||
// ============================================================
|
||||
// MemPack canonically on _All_True (see FastestByte-mode comment above for rationale).
|
||||
// MemPack uses _All_False (see FastestByte-mode comment above for rationale).
|
||||
new MemoryPackBenchmark<TestOrder_All_False>(orderFalse, Configuration.SelectedWireMode, "Default"),
|
||||
new MemoryPackBufferWriterBenchmark<TestOrder_All_False>(orderFalse, Configuration.SelectedWireMode, "Default"),
|
||||
new MemoryPackFreshBufferWriterBenchmark<TestOrder_All_False>(orderFalse, Configuration.SelectedWireMode, "Default"),
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ namespace AyCode.Core.Serializers.Console;
|
|||
/// </summary>
|
||||
internal static class Configuration
|
||||
{
|
||||
internal const string ResultsDirectory = @"H:\Applications\Aycode\Source\AyCode.Core\Test_Benchmark_Results\Benchmark";
|
||||
|
||||
#if AYCODE_NATIVEAOT
|
||||
internal const string BuildConfiguration = "NativeAOT";
|
||||
#elif DEBUG
|
||||
|
|
|
|||
|
|
@ -1,30 +1,82 @@
|
|||
# AyCode.Core.Serializers.Console
|
||||
|
||||
Standalone benchmark console application for comparing serializer performance. Targets .NET 9. Measures serialize/deserialize speed, output size, and compression across multiple serializers and data shapes.
|
||||
Interactive console runner for the serializer benchmark suite. Targets .NET 9.
|
||||
|
||||
## Compared Serializers
|
||||
> **Companion**: shares its workload + reporting infrastructure with the BDN runner in [`AyCode.Benchmark/`](../AyCode.Benchmark/README.md) via `<ProjectReference>`. See that project's README for the full dual-runner architecture.
|
||||
|
||||
- **AcBinary** — Multiple configurations: Default, NoRef, FastMode, NoIntern, with/without source generation
|
||||
- **MessagePack**
|
||||
- **MemoryPack**
|
||||
## Role
|
||||
|
||||
(System.Text.Json and Newtonsoft.Json comparisons exist but are currently commented out.)
|
||||
This is the **fast-iteration** half of the benchmark stack — a custom adaptive measure engine optimized for short turnaround (~1-3 min full run) during micro-optimization loops. The BDN half lives in `AyCode.Benchmark` and produces statistically tighter numbers (~5-15 min full run) for before-commit validation. Both runners emit the **same** `.log` / `.LLM` / `.output` triplet to `Test_Benchmark_Results/Benchmark/` — Console prefixes with `Console.`, BDN with `Bdn.`.
|
||||
|
||||
## Key Files
|
||||
## Compared serializers
|
||||
|
||||
- **`Program.cs`** — Benchmark runner. Modes: `all` (default), `quick` (fewer iterations), `serialize`, `deserialize`. Outputs results to `Test_Benchmark_Results/Benchmark/`. Iterations: 5000 warmup + 1000 test (Release), 0+1 (Debug).
|
||||
- **`BenchmarkTestDataProvider.cs`** — Test data factory producing 5 data shapes:
|
||||
- Small (2x2x2x2), Medium (3x3x3x4), Large (5x5x5x10)
|
||||
- Repeated Strings (10 items, string deduplication testing)
|
||||
- Deep Nested (2x4x4x8, depth stress test)
|
||||
- Uses `TestOrder` model from `AyCode.Core.Tests` with configurable IId reference percentages.
|
||||
- **AcBinary** — multiple options presets: `FastMode` (Compact wire, no ref handling, no interning), `Default` (with ref handling + interning), plus SGen / Runtime dispatch variants and Compact / Fast wire modes.
|
||||
- **MemoryPack** — SOTA baseline, wire-mode-aligned with AcBinary for apples-to-apples encoding comparison (UTF-8 ↔ Compact, UTF-16 ↔ Fast).
|
||||
- **MessagePack** — JIT-only (AOT incompatible due to dynamic resolver).
|
||||
- **System.Text.Json** — reference comparison (commented out in `CreateSerializers` by default).
|
||||
|
||||
## Key files
|
||||
|
||||
- [`Program.cs`](Program.cs) — entry point. Parses CLI args (`Core` / `Comprehensive` / `Edge` / per-cell / op-mode / serializer-set) or falls into interactive `Menu`.
|
||||
- [`Menu.cs`](Menu.cs) — interactive layer/serializer-set selection + nested settings (iteration counts, wire mode, charset).
|
||||
- [`BenchmarkLoop.cs`](BenchmarkLoop.cs) — custom adaptive measure engine. CPU 0 affinity pin + High priority for stabilization, JIT pre-warmup, phase-isolated Ser/Des warmup→measure with `GC.Collect` at every boundary, 10-sample median + pilot discard, adaptive iter calibration to ~250ms/cell wall-clock, dedicated allocation-only sample.
|
||||
- [`Configuration.cs`](Configuration.cs) — Console-side state (`SelectedWireMode`, `WarmupIterations`, `BenchmarkSamples`, `TargetSampleMs`, charset selection, `BuildConfiguration` const from `#if DEBUG/RELEASE/AYCODE_NATIVEAOT`).
|
||||
|
||||
Workload + reporting types — `ISerializerBenchmark`, `BenchmarkResult`, `BenchmarkOptions`, `BenchmarkEnums`, `BenchmarkReportWriter`, `ReportingContext`, the 12 concrete `*Benchmark<T>` classes (`AcBinaryBenchmark`, `MemoryPackBenchmark`, `AcBinaryBufferWriterBenchmark`, ...), `RoundTripValidator` — live in [`AyCode.Benchmark/Workloads/Scenarios/`](../AyCode.Benchmark/Workloads/Scenarios/) and [`AyCode.Benchmark/Reporting/`](../AyCode.Benchmark/Reporting/).
|
||||
|
||||
## Test data
|
||||
|
||||
5 cells, provided by `AyCode.Core.Tests.TestModels.BenchmarkTestDataProvider*`:
|
||||
|
||||
- **Small** (2×2×2×2)
|
||||
- **Medium** (3×3×3×4)
|
||||
- **Large** (5×5×5×10)
|
||||
- **Repeated Strings** (10 items, string-deduplication stress)
|
||||
- **Deep Nested** (2×4×4×8, depth stress)
|
||||
|
||||
20% IId reference rate by default. Two graph variants (`TestOrder_All_False` / `_All_True`) are built per cell — AcBinary's option preset picks which variant gets fed to it (`UsesAllFalseVariant` rule in `BenchmarkLoop`).
|
||||
|
||||
## Charset profiles (Menu → Settings → Charset)
|
||||
|
||||
Controls the `BenchmarkTestDataProvider.LongStringSuffix` — the string-tail appended to property values. Influences string-marker selection on the wire (FixStr vs StringSmall / Medium / Big), interning hit rates, and UTF-8 encode cost.
|
||||
|
||||
| Profile | Content |
|
||||
|---|---|
|
||||
| `Latin1FixAscii` | Empty suffix (short FixStr fast-path stress) |
|
||||
| `Latin1Short` | "árvíztűrő tükörfúrógép" (~24 char Hungarian mixed) |
|
||||
| `Latin1Long` | ~47-char Latin1 mixed (default) |
|
||||
| `CjkBmp` | CJK BMP (3-byte UTF-8 runs) |
|
||||
| `Cyrillic` | Russian Cyrillic (2-byte UTF-8 runs) |
|
||||
| `Mixed` | Hungarian + CJK + Cyrillic + emoji (full-spectrum + surrogate pairs) |
|
||||
|
||||
## CLI
|
||||
|
||||
```
|
||||
dotnet run -c Release --project AyCode.Core.Serializers.Console -- [arg]
|
||||
```
|
||||
|
||||
| Arg | Result |
|
||||
|---|---|
|
||||
| _(no args)_ | Interactive menu — pick layer (Core / Comprehensive / Edge / Small / Medium / Large / Repeated / Deep / All) × serializer-set (Standard / FastestByte ["F"] / AsyncPipe ["P"]). |
|
||||
| `Core` / `Comprehensive` / `Edge` / `Small` / `Medium` / `Large` / `Repeated` / `Deep` / `All` | Run that layer at `Standard` serializer-set, `All` op-mode. |
|
||||
| `FastestByte` / `AsyncPipe` / `Standard` | Run that serializer-set, `All` layer, `All` op-mode. |
|
||||
| `Serialize` / `Deserialize` / `All` | Run that op-mode, `All` layer, `Standard` serializer-set. |
|
||||
| `quick` | Single-sample fast mode (Debug-equivalent — very loose numbers, smoke-test only). |
|
||||
|
||||
Output: `Test_Benchmark_Results/Benchmark/Console.FullBenchmark_<Build>_<timestamp>.{log,LLM,output}`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Dependency | Purpose |
|
||||
|---|---|
|
||||
| `AyCode.Core` | Core library with AcBinary serializer |
|
||||
| `AyCode.Core.Tests` | Test models (`TestOrder`, `TestDataFactory`, etc.) |
|
||||
| `MemoryPack` | Competitor benchmark |
|
||||
| `MessagePack` | Competitor benchmark |
|
||||
| `Newtonsoft.Json` | Competitor benchmark |
|
||||
| `AyCode.Core` (ProjectReference) | AcBinary serializer |
|
||||
| `AyCode.Core.Tests` (ProjectReference) | Test data factory + test models |
|
||||
| `AyCode.Benchmark` (ProjectReference) | Shared workload + reporting (`ISerializerBenchmark`, `BenchmarkResult`, `BenchmarkReportWriter`, `ReportingContext`, the 12 concrete benchmark classes) |
|
||||
| `MemoryPack` | Comparison target (also via Workloads) |
|
||||
| `MessagePack` | Comparison target |
|
||||
| `Newtonsoft.Json` | Comparison target (currently disabled) |
|
||||
|
||||
## Build & publish notes
|
||||
|
||||
- `<StartupObject>AyCode.Core.Serializers.Console.Program</StartupObject>` in the csproj explicitly disambiguates the entry point — necessary because this Exe references another Exe (`AyCode.Benchmark`), and the build would otherwise complain about multiple `Main` methods.
|
||||
- AOT publish (`dotnet publish -c Release`) is configured via `'$(_IsPublishing)' == 'true'` PropertyGroup. The Benchmark project's BDN-stack (BenchmarkDotNet, Iced disassembler, MongoDB.Bson) is pulled in transitively — accepted tradeoff for the unified workload sharing.
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,17 @@
|
|||
# BINARY — TODO archive (2026-04)
|
||||
|
||||
Archived entries from `BINARY_TODO.md` per LLMP-DEC retention policy. IDs preserved (never reassigned). Format identical to active file.
|
||||
|
||||
## ACCORE-BIN-T-S8P4: Replace JSON-in-Binary request parameters
|
||||
**Priority:** P1 · **Type:** Refactor · **Status:** Closed (2026-04-26, landed in commits `cdd54d3` 2026-04-05 + `3b70070` 2026-04-06) · **Related:** `../XCUT/XCUT_ISSUES.md#accore-xcut-i-x8q1` (canonical), `AyCode.Services/docs/SIGNALR/SIGNALR_TODO.md`
|
||||
|
||||
Migrate client→server request parameters from JSON-in-Binary envelope to direct Binary serialization (matching response path). Coordinated change across client, server, and all consuming projects. Do NOT attempt as side-effect of unrelated work.
|
||||
|
||||
**Acceptance:** `SignalPostJsonDataMessage<T>` replaced by a `SignalPostBinaryDataMessage<T>` (or equivalent); no JSON round-trip on the wire for request params; benchmarks confirm no regression.
|
||||
|
||||
### Resolution
|
||||
- **What:** Length-prefixed, per-parameter binary format introduced via `SignalRSerializationHelper.SerializeParametersToBinary` / `DeserializeParametersFromBinary`; further unified into `SignalParams` (single `byte[]` carrying packed method parameters with `SetParameterValues` / `GetParameterValues`).
|
||||
- **Where:** `AyCode.Services/SignalRs/AcSignalRClientBase.cs`, `AcWebSignalRHubBase.cs`, `ISignalParams.cs` (server + client dispatch); `IAcSignalRHubClient.cs` (legacy wrappers).
|
||||
- **Equivalent (not literal `SignalPostBinaryDataMessage<T>`):** `SignalParams` was chosen over a 1:1 binary wrapper class — fewer indirections on the hot path, type-safe pack/unpack, and `DataSerializerType` field on `SignalReceiveParams` for response format indication.
|
||||
- **Wire impact:** No JSON round-trip on the wire for request params; this is a **breaking change** vs. previous JSON-in-Binary clients/servers (see commit message).
|
||||
- **Legacy types:** `SignalPostJsonMessage`, `SignalPostJsonDataMessage<T>`, `SignalPostMessage<T>`, `ISignalPostMessage<T>` all marked `[Obsolete]` in `IAcSignalRHubClient.cs`; deletion tracked separately in `AyCode.Services/docs/SIGNALR/SIGNALR_TODO.md#accore-sig-t-s3n8` (gated on consumer migration).
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -11,6 +11,7 @@ AcBinary serialization system. Primary goal: **speed** (two-phase scan+serialize
|
|||
- [`BINARY_IMPLEMENTATION.md`](BINARY_IMPLEMENTATION.md) — Internal implementation details
|
||||
- [`BINARY_WRITERS.md`](BINARY_WRITERS.md) — Writer internals (streaming, buffering)
|
||||
- [`BINARY_SGEN.md`](BINARY_SGEN.md) — Source generator (`AyCode.Core.Serializers.SourceGenerator`)
|
||||
- [`BINARY_SGEN_OPTIMIZATION.md`](BINARY_SGEN_OPTIMIZATION.md) — SGen per-property emit micro-optimization brainstorming / methodology notes (working doc, not a TODO)
|
||||
- [`BINARY_ISSUES.md`](BINARY_ISSUES.md) — Known issues and limitations (binary serializer core)
|
||||
- [`BINARY_TODO.md`](BINARY_TODO.md) — Planned work / open tickets (binary serializer core)
|
||||
- [`BINARY_ASYNCPIPE_ISSUES.md`](BINARY_ASYNCPIPE_ISSUES.md) — Known issues and limitations (streaming I/O layer: `AsyncPipeReaderInput` + `AsyncPipeWriterOutput`)
|
||||
|
|
|
|||
Loading…
Reference in New Issue