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:
Loretta 2026-05-15 20:54:42 +02:00
parent ed03d754ec
commit c611d4b535
16 changed files with 1521 additions and 1514 deletions

View File

@ -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();
}

View File

@ -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&lt;AcBinaryVsMemPackBenchmark&gt;(...)</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");
}
}

View File

@ -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>

View File

@ -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 |

View File

@ -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; }

View File

@ -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.

View File

@ -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);
}

View File

@ -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.

View File

@ -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"),

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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`)