diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f06fff6..d9c4a2c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -103,7 +103,8 @@ "Bash(awk 'NR < 69 || NR > 1257 { print }' AcBinarySourceGenerator.cs)", "Bash(awk 'NR == 68 { print; print \"\"; print \" // Writer-side emit pass \\(GenWriter + GenScanProperties + EmitProp + EmitScan* + EmitDirect*Write +\"; print \" // EmitSkip + EmitVal + EmitMarkerless + helpers\\) moved to AcBinarySourceGenerator.GenWriter.cs.\"; next } { print }' tmp.cs)", "Bash(awk 'NR < 73 || NR > 930 { print }' AcBinarySourceGenerator.cs)", - "Bash(awk 'NR == 72 { print; print \" // Reader-side emit pass \\(GenReader + EmitReadProp + EmitRead* helpers\\) moved to\"; print \" // AcBinarySourceGenerator.GenReader.cs.\"; next } { print }' tmp.cs)" + "Bash(awk 'NR == 72 { print; print \" // Reader-side emit pass \\(GenReader + EmitReadProp + EmitRead* helpers\\) moved to\"; print \" // AcBinarySourceGenerator.GenReader.cs.\"; next } { print }' tmp.cs)", + "Bash(rm -f \"AyCode.Core.Serializers.Console/Benchmarks/\"*.cs && rmdir \"AyCode.Core.Serializers.Console/Benchmarks\" && rm -f \"AyCode.Core.Serializers.Console/BenchmarkResult.cs\" && echo \"Deleted Console-side moved files.\")" ] } } diff --git a/AyCode.Benchmark/AyCode.Benchmark.csproj b/AyCode.Benchmark/AyCode.Benchmark.csproj index 55f04c5..8976b96 100644 --- a/AyCode.Benchmark/AyCode.Benchmark.csproj +++ b/AyCode.Benchmark/AyCode.Benchmark.csproj @@ -17,6 +17,7 @@ + diff --git a/AyCode.Core.Serializers.Console/BenchmarkResult.cs b/AyCode.Benchmark/Reporting/BenchmarkResult.cs similarity index 91% rename from AyCode.Core.Serializers.Console/BenchmarkResult.cs rename to AyCode.Benchmark/Reporting/BenchmarkResult.cs index 0ea10f8..48ac97f 100644 --- a/AyCode.Core.Serializers.Console/BenchmarkResult.cs +++ b/AyCode.Benchmark/Reporting/BenchmarkResult.cs @@ -1,13 +1,14 @@ -using AyCode.Core.Serializers.Console.Benchmarks; +using AyCode.Core.Benchmarks.Workloads.Scenarios; -namespace AyCode.Core.Serializers.Console; +namespace AyCode.Core.Benchmarks.Reporting; /// -/// Per-cell benchmark result row. Populated by the benchmark execution loop in -/// BenchmarkLoop.RunBenchmarksForTestData; consumed by the output formatters in -/// Output (console table + .log + .LLM file writers). Pure DTO — no behaviour. +/// Per-cell benchmark result row. Populated by the benchmark execution loop (Console-side +/// BenchmarkLoop.RunBenchmarksForTestData / BDN-side BdnSummaryAdapter); consumed by the +/// output formatters in BenchmarkReportWriter (console table + .log + .LLM file writers). +/// Pure DTO — no behaviour. /// -internal sealed class BenchmarkResult +public sealed class BenchmarkResult { public string TestDataName { get; set; } = ""; public BenchmarkEngine Engine { get; set; } @@ -18,7 +19,7 @@ internal sealed class BenchmarkResult /// /// CLR type name of the order graph serialised in this row (e.g. "TestOrder_All_False", /// "TestOrder_All_True"). Captured from in - /// RunBenchmarksForTestData; surfaced in the SERIALIZER OPTIONS section of every output + /// the runner loop; surfaced in the SERIALIZER OPTIONS section of every output /// (.log, .LLM, console) so the reader can correlate each preset with its TestOrder variant /// without inflating the per-row tables with an extra column. /// diff --git a/AyCode.Benchmark/Reporting/ReportingContext.cs b/AyCode.Benchmark/Reporting/ReportingContext.cs new file mode 100644 index 0000000..566e01d --- /dev/null +++ b/AyCode.Benchmark/Reporting/ReportingContext.cs @@ -0,0 +1,47 @@ +using System.Text; + +namespace AyCode.Core.Benchmarks.Reporting; + +/// +/// Context bundle for the unified benchmark report writer. Same record on both sides (Console / BDN), +/// only the differs ("Console" / "Bdn") — that drives the filename prefix +/// (e.g. Console.FullBenchmark_Release_{timestamp}.LLM vs Bdn.FullBenchmark_Release_{timestamp}.LLM). +/// The resolution walks up from to the +/// nearest AyCode.Core.sln and combines with Test_Benchmark_Results\Benchmark — works across +/// build modes (Debug / Release / AOT publish) and worktrees (each worktree has its own .sln, so its bench +/// results land alongside its code). +/// +public sealed record ReportingContext( + string SourceTag, + string ResultsDirectory, + string BuildConfiguration, + UTF8Encoding Utf8NoBom) +{ + /// + /// Resolves the canonical by walking up from + /// to the nearest AyCode.Core.sln, then combining + /// with Test_Benchmark_Results\Benchmark. The build configuration is supplied by the caller + /// (Console resolves via #if AYCODE_NATIVEAOT|DEBUG|else Release; BDN-side currently mirrors + /// the JIT vs AOT discriminator from ). + /// + public static ReportingContext Create(string sourceTag, string buildConfiguration) => + new(sourceTag, ResolveResultsDirectory(), buildConfiguration, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + + /// + /// Walk-up from the assembly's BaseDirectory to find the repo root (marker: AyCode.Core.sln). + /// Returns {repoRoot}\Test_Benchmark_Results\Benchmark. Worktree-aware: if running from a + /// worktree, the walk finds the worktree's own .sln (each worktree has its own checkout), so + /// results land in the worktree's results folder — the natural place when the worktree's code + /// changes are what produced the numbers. + /// + private static string ResolveResultsDirectory() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir != null && !File.Exists(Path.Combine(dir.FullName, "AyCode.Core.sln"))) + dir = dir.Parent; + if (dir == null) + throw new InvalidOperationException( + "Cannot locate repo root (AyCode.Core.sln) from AppContext.BaseDirectory: " + AppContext.BaseDirectory); + return Path.Combine(dir.FullName, "Test_Benchmark_Results", "Benchmark"); + } +} diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBenchmark.cs b/AyCode.Benchmark/Workloads/Scenarios/AcBinaryBenchmark.cs similarity index 83% rename from AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBenchmark.cs rename to AyCode.Benchmark/Workloads/Scenarios/AcBinaryBenchmark.cs index 6f6db10..7e0d56a 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBenchmark.cs +++ b/AyCode.Benchmark/Workloads/Scenarios/AcBinaryBenchmark.cs @@ -1,14 +1,14 @@ -using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.TestModels; using System.Runtime.CompilerServices; -namespace AyCode.Core.Serializers.Console.Benchmarks; +namespace AyCode.Core.Benchmarks.Workloads.Scenarios; /// /// AcBinary benchmark, Byte[] I/O mode. The headline AcBinary row in every cell — compared -/// against as the SOTA baseline. +/// against as the SOTA baseline. /// -internal sealed class AcBinaryBenchmark : ISerializerBenchmark where T : class +public sealed class AcBinaryBenchmark : ISerializerBenchmark where T : class { private readonly T _order; private readonly AcBinarySerializerOptions _options; @@ -42,6 +42,6 @@ internal sealed class AcBinaryBenchmark : ISerializerBenchmark where T : clas { var bytes = AcBinarySerializer.Serialize(_order, _options); var roundTripped = AcBinaryDeserializer.Deserialize(bytes, _options); - return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped); + return RoundTripValidator.DeepEqualsViaJson(_order, roundTripped); } } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBufferWriterBenchmark.cs b/AyCode.Benchmark/Workloads/Scenarios/AcBinaryBufferWriterBenchmark.cs similarity index 92% rename from AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBufferWriterBenchmark.cs rename to AyCode.Benchmark/Workloads/Scenarios/AcBinaryBufferWriterBenchmark.cs index c8b6163..ffaf4e9 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryBufferWriterBenchmark.cs +++ b/AyCode.Benchmark/Workloads/Scenarios/AcBinaryBufferWriterBenchmark.cs @@ -1,15 +1,15 @@ -using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.TestModels; using System.Buffers; using System.Runtime.CompilerServices; -namespace AyCode.Core.Serializers.Console.Benchmarks; +namespace AyCode.Core.Benchmarks.Workloads.Scenarios; /// /// Benchmarks AcBinary via the IBufferWriter overload with a pre-allocated, reused ArrayBufferWriter. /// Realistic IBufferWriter usage pattern: caller owns + reuses the writer (zero alloc per call after warmup). /// -internal sealed class AcBinaryBufferWriterBenchmark : ISerializerBenchmark where T : class +public sealed class AcBinaryBufferWriterBenchmark : ISerializerBenchmark where T : class { private readonly T _order; private readonly AcBinarySerializerOptions _options; @@ -64,6 +64,6 @@ internal sealed class AcBinaryBufferWriterBenchmark : ISerializerBenchmark wh AcBinarySerializer.Serialize(_order, _bufferWriter, _options); var roundTripped = AcBinaryDeserializer.Deserialize(new ReadOnlySequence(_bufferWriter.WrittenMemory), _options); - return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped); + return RoundTripValidator.DeepEqualsViaJson(_order, roundTripped); } } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryFreshBufferWriterBenchmark.cs b/AyCode.Benchmark/Workloads/Scenarios/AcBinaryFreshBufferWriterBenchmark.cs similarity index 91% rename from AyCode.Core.Serializers.Console/Benchmarks/AcBinaryFreshBufferWriterBenchmark.cs rename to AyCode.Benchmark/Workloads/Scenarios/AcBinaryFreshBufferWriterBenchmark.cs index 3aa20bd..2849d18 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryFreshBufferWriterBenchmark.cs +++ b/AyCode.Benchmark/Workloads/Scenarios/AcBinaryFreshBufferWriterBenchmark.cs @@ -1,9 +1,9 @@ -using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.TestModels; using System.Buffers; using System.Runtime.CompilerServices; -namespace AyCode.Core.Serializers.Console.Benchmarks; +namespace AyCode.Core.Benchmarks.Workloads.Scenarios; /// /// Benchmarks AcBinary via the IBufferWriter overload, allocating a FRESH ArrayBufferWriter on EVERY call. @@ -12,7 +12,7 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// otherwise AcBinary would request 64KB upfront via GetSpan(), forcing the fresh ABW to allocate 64KB /// regardless of payload size (heavy over-allocation for small payloads). /// -internal sealed class AcBinaryFreshBufferWriterBenchmark : ISerializerBenchmark where T : class +public sealed class AcBinaryFreshBufferWriterBenchmark : ISerializerBenchmark where T : class { private readonly T _order; private readonly AcBinarySerializerOptions _options; @@ -59,6 +59,6 @@ internal sealed class AcBinaryFreshBufferWriterBenchmark : ISerializerBenchma var abw = new ArrayBufferWriter(); AcBinarySerializer.Serialize(_order, abw, _options); var roundTripped = AcBinaryDeserializer.Deserialize(new ReadOnlySequence(abw.WrittenMemory), _options); - return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped); + return RoundTripValidator.DeepEqualsViaJson(_order, roundTripped); } } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryPipeBenchmark.cs b/AyCode.Benchmark/Workloads/Scenarios/AcBinaryInMemoryPipeBenchmark.cs similarity index 95% rename from AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryPipeBenchmark.cs rename to AyCode.Benchmark/Workloads/Scenarios/AcBinaryInMemoryPipeBenchmark.cs index 20a0d28..bd20607 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryPipeBenchmark.cs +++ b/AyCode.Benchmark/Workloads/Scenarios/AcBinaryInMemoryPipeBenchmark.cs @@ -1,13 +1,13 @@ -using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.Serialization; // DrainFromAsync extension (test-only, used by benchmark) using AyCode.Core.Tests.TestModels; using System.IO.Pipelines; using System.Runtime.CompilerServices; -namespace AyCode.Core.Serializers.Console.Benchmarks; +namespace AyCode.Core.Benchmarks.Workloads.Scenarios; /// -/// Same chunked-framed AsyncPipe code path as , but the transport +/// Same chunked-framed AsyncPipe code path as , but the transport /// is an in-memory instead of a kernel NamedPipe. The Pipe's /// Writer/Reader pair is a managed-only zero-copy slab handoff — no syscalls, no kernel /// buffer copy, no IRP queueing. @@ -15,16 +15,16 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// Why this benchmark matters: by holding ALL other variables constant (same SerializeChunkedFramed, /// same AsyncPipeReaderInput, same drain task, same consumer task, same multi-message wire format), this /// row isolates the kernel-NamedPipe transport overhead from the chunked-streaming framework's pure -/// CPU cost. The expected delta vs : per-chunk overhead drops from +/// CPU cost. The expected delta vs : per-chunk overhead drops from /// ~25-30 µs (kernel-syscall pair + IRP) to ~1-2 µs (managed slab handoff). Multi-chunk Large-message rows -/// should converge dramatically toward . +/// should converge dramatically toward . /// /// Real-world relevance: in-memory Pipe is the typical primitive used for cross-thread serializer /// pipelines inside a single process (e.g. SignalR's Kestrel transport adapter, gRPC framework internals, /// custom message brokers). The numbers from this row reflect that scenario, NOT the kernel-pipe loopback /// of the NamedPipe benchmark. /// -internal sealed class AcBinaryInMemoryPipeBenchmark : ISerializerBenchmark, IDisposable where T : class +public sealed class AcBinaryInMemoryPipeBenchmark : ISerializerBenchmark, IDisposable where T : class { private readonly T _order; private readonly AcBinarySerializerOptions _options; @@ -160,7 +160,7 @@ internal sealed class AcBinaryInMemoryPipeBenchmark : ISerializerBenchmark, I { Serialize(); var result = _lastResult as T; - return result != null && BenchmarkLoop.DeepEqualsViaJson(_order, result); + return result != null && RoundTripValidator.DeepEqualsViaJson(_order, result); } finally { diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryRawByteArrayBenchmark.cs b/AyCode.Benchmark/Workloads/Scenarios/AcBinaryInMemoryRawByteArrayBenchmark.cs similarity index 92% rename from AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryRawByteArrayBenchmark.cs rename to AyCode.Benchmark/Workloads/Scenarios/AcBinaryInMemoryRawByteArrayBenchmark.cs index fd44e30..31b1558 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryInMemoryRawByteArrayBenchmark.cs +++ b/AyCode.Benchmark/Workloads/Scenarios/AcBinaryInMemoryRawByteArrayBenchmark.cs @@ -1,8 +1,8 @@ -using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.TestModels; using System.Runtime.CompilerServices; -namespace AyCode.Core.Serializers.Console.Benchmarks; +namespace AyCode.Core.Benchmarks.Workloads.Scenarios; /// /// Raw byte[] over an in-memory cross-thread handoff — NO transport (no NamedPipe, no Pipe, no @@ -11,17 +11,17 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// /// Why this benchmark matters: completes the 2x2 transport × wire-format matrix: /// -/// NamedPipe + Chunked = -/// NamedPipe + Raw = -/// In-memory Pipe + Chunked = +/// NamedPipe + Chunked = +/// NamedPipe + Raw = +/// In-memory Pipe + Chunked = /// In-memory + Raw = THIS row — apples-to-apples baseline for the in-memory chunked row /// -/// Side-by-side with this isolates the chunked-streaming +/// Side-by-side with this isolates the chunked-streaming /// framework's pure CPU cost, with the same in-memory transport (zero kernel involvement) on both sides. -/// Side-by-side with this isolates the kernel-NamedPipe +/// Side-by-side with this isolates the kernel-NamedPipe /// overhead on the raw-byte[] side. /// -internal sealed class AcBinaryInMemoryRawByteArrayBenchmark : ISerializerBenchmark, IDisposable where T : class +public sealed class AcBinaryInMemoryRawByteArrayBenchmark : ISerializerBenchmark, IDisposable where T : class { private readonly T _order; private readonly AcBinarySerializerOptions _options; @@ -144,7 +144,7 @@ internal sealed class AcBinaryInMemoryRawByteArrayBenchmark : ISerializerBenc { Serialize(); var result = _lastResult as T; - return result != null && BenchmarkLoop.DeepEqualsViaJson(_order, result); + return result != null && RoundTripValidator.DeepEqualsViaJson(_order, result); } finally { diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeBenchmark.cs b/AyCode.Benchmark/Workloads/Scenarios/AcBinaryNamedPipeBenchmark.cs similarity index 97% rename from AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeBenchmark.cs rename to AyCode.Benchmark/Workloads/Scenarios/AcBinaryNamedPipeBenchmark.cs index 6127173..65f29e3 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeBenchmark.cs +++ b/AyCode.Benchmark/Workloads/Scenarios/AcBinaryNamedPipeBenchmark.cs @@ -1,11 +1,11 @@ -using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.Serialization; // DrainFromAsync extension (test-only, used by benchmark) using AyCode.Core.Tests.TestModels; using System.IO.Pipelines; using System.IO.Pipes; using System.Runtime.CompilerServices; -namespace AyCode.Core.Serializers.Console.Benchmarks; +namespace AyCode.Core.Benchmarks.Workloads.Scenarios; /// /// Benchmarks AcBinary over a long-lived NamedPipe IPC connection using the AcBinary native streaming API @@ -39,7 +39,7 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// Approximation note: single-process loopback NamedPipe. Real cross-process / cross-machine SignalR /// adds further transport latency (TCP, WebSocket framing) on top. The benchmark gives a lower bound. /// -internal sealed class AcBinaryNamedPipeBenchmark : ISerializerBenchmark, IDisposable where T : class +public sealed class AcBinaryNamedPipeBenchmark : ISerializerBenchmark, IDisposable where T : class { private readonly T _order; private readonly AcBinarySerializerOptions _options; @@ -205,7 +205,7 @@ internal sealed class AcBinaryNamedPipeBenchmark : ISerializerBenchmark, IDis { Serialize(); var result = _lastResult as T; - return result != null && BenchmarkLoop.DeepEqualsViaJson(_order, result); + return result != null && RoundTripValidator.DeepEqualsViaJson(_order, result); } finally { diff --git a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeRawByteArrayBenchmark.cs b/AyCode.Benchmark/Workloads/Scenarios/AcBinaryNamedPipeRawByteArrayBenchmark.cs similarity index 94% rename from AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeRawByteArrayBenchmark.cs rename to AyCode.Benchmark/Workloads/Scenarios/AcBinaryNamedPipeRawByteArrayBenchmark.cs index a45c8d0..55d6152 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/AcBinaryNamedPipeRawByteArrayBenchmark.cs +++ b/AyCode.Benchmark/Workloads/Scenarios/AcBinaryNamedPipeRawByteArrayBenchmark.cs @@ -1,9 +1,9 @@ -using AyCode.Core.Serializers.Binaries; +using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.TestModels; using System.IO.Pipes; using System.Runtime.CompilerServices; -namespace AyCode.Core.Serializers.Console.Benchmarks; +namespace AyCode.Core.Benchmarks.Workloads.Scenarios; /// /// Raw byte[] over a long-lived NamedPipe — NO chunk-framing, NO AsyncPipeReaderInput, @@ -11,21 +11,21 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// reads and deserialises. Two-task pattern enables Ser↔Read overlap (kernel-pipe-pipelined) AND /// avoids the kernel-buffer-full deadlock when bytes.Length > inBufferSize. /// -/// Side-by-side with (chunked-framed AsyncPipe stack) this +/// Side-by-side with (chunked-framed AsyncPipe stack) this /// isolates two cost components on the SAME kernel-pipe transport with the SAME inBufferSize: /// -/// This row vs (Byte[]) — pure kernel-NamedPipe +/// This row vs (Byte[]) — pure kernel-NamedPipe /// overhead (WriteFile / ReadFile syscalls + IRP queueing + buffer-copy + thread-handoff). -/// This row vs (chunked-framed) — pure +/// This row vs (chunked-framed) — pure /// AsyncPipe-framework overhead (chunk header writes + sliding-window Feed + MRES wait inside /// AsyncPipeReaderInput) AND the streaming-pipeline benefit of intra-message Ser↔Des overlap (which /// raw lacks — raw can only Ser↔Read overlap, with Des sequential after Read completes). /// /// Per-iter byte[] allocation from AcBinarySerializer.Serialize is part of the cost (matches -/// 's API contract); the receive-side scratch buffer is also allocated per-iter +/// 's API contract); the receive-side scratch buffer is also allocated per-iter /// on the consumer-task (counted via GC.GetTotalAllocatedBytes in BenchmarkLoop.MeasureAllocationTotal). /// -internal sealed class AcBinaryNamedPipeRawByteArrayBenchmark : ISerializerBenchmark, IDisposable where T : class +public sealed class AcBinaryNamedPipeRawByteArrayBenchmark : ISerializerBenchmark, IDisposable where T : class { private readonly T _order; private readonly AcBinarySerializerOptions _options; @@ -185,7 +185,7 @@ internal sealed class AcBinaryNamedPipeRawByteArrayBenchmark : ISerializerBen { Serialize(); var result = _lastResult as T; - return result != null && BenchmarkLoop.DeepEqualsViaJson(_order, result); + return result != null && RoundTripValidator.DeepEqualsViaJson(_order, result); } finally { diff --git a/AyCode.Core.Serializers.Console/Benchmarks/BenchmarkEnums.cs b/AyCode.Benchmark/Workloads/Scenarios/BenchmarkEnums.cs similarity index 86% rename from AyCode.Core.Serializers.Console/Benchmarks/BenchmarkEnums.cs rename to AyCode.Benchmark/Workloads/Scenarios/BenchmarkEnums.cs index 4a107f9..314da61 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/BenchmarkEnums.cs +++ b/AyCode.Benchmark/Workloads/Scenarios/BenchmarkEnums.cs @@ -1,11 +1,11 @@ -namespace AyCode.Core.Serializers.Console.Benchmarks; +namespace AyCode.Core.Benchmarks.Workloads.Scenarios; /// /// Serializer engine identifier — replaces the prior Configuration.EngineXxx string constants /// with a type-safe enum. The benchmark-result Engine column uses for /// the human-readable form. /// -internal enum BenchmarkEngine +public enum BenchmarkEngine { AcBinary, MemoryPack, @@ -21,7 +21,7 @@ internal enum BenchmarkEngine /// (they distinguish chunked-framed vs raw-byte[] semantics, but render identically in the IO column); /// the same applies to + ("Pipe(in-mem)"). /// -internal enum BenchmarkIoMode +public enum BenchmarkIoMode { ByteArray, BufWrReuse, @@ -42,7 +42,7 @@ internal enum BenchmarkIoMode /// — SGen root with non-SGen child types reached via bridge methods (see docs/BINARY/BINARY_SGEN.md). /// /// -internal enum BenchmarkDispatchMode +public enum BenchmarkDispatchMode { SGen, Runtime, @@ -50,7 +50,7 @@ internal enum BenchmarkDispatchMode } /// -/// Test-data layer filter — selects which cells participate in the run. +/// Test-data layer filter — selects which test data cells participate in the run. /// Replaces the prior string-typed layer parameter; CLI/menu callers parse user input via /// with ignoreCase: true. /// @@ -59,7 +59,7 @@ internal enum BenchmarkDispatchMode /// //// — single-cell mini-suites for tight A/B iteration loops. /// /// -internal enum BenchmarkLayer +public enum BenchmarkLayer { All, Core, @@ -78,7 +78,7 @@ internal enum BenchmarkLayer /// a no-op and only run on or . Replaces the prior string-typed /// mode/opMode parameter. /// -internal enum BenchmarkOpMode +public enum BenchmarkOpMode { All, Serialize, @@ -86,7 +86,7 @@ internal enum BenchmarkOpMode } /// -/// Serializer-set selection — drives BenchmarkLoop.CreateSerializers to return one of three +/// Serializer-set selection — drives the runner's serializer-factory to return one of three /// preset bundles instead of a magic string. Replaces the prior string-typed serializerMode /// parameter. /// @@ -95,7 +95,7 @@ internal enum BenchmarkOpMode /// — streaming I/O isolation (NamedPipe + in-memory Pipe variants only). /// /// -internal enum SerializerSelectionMode +public enum SerializerSelectionMode { Standard, FastestByte, @@ -107,9 +107,9 @@ internal enum SerializerSelectionMode /// human-readable form used by the per-row console table, the .log file CSV/formatted output, /// and the .LLM markdown table. Centralised here so every output formatter renders identically. /// -internal static class BenchmarkEnumExtensions +public static class BenchmarkEnumExtensions { - internal static string ToDisplay(this BenchmarkEngine engine) => engine switch + public static string ToDisplay(this BenchmarkEngine engine) => engine switch { BenchmarkEngine.AcBinary => "AcBinary", BenchmarkEngine.MemoryPack => "MemoryPack", @@ -120,7 +120,7 @@ internal static class BenchmarkEnumExtensions _ => throw new ArgumentOutOfRangeException(nameof(engine), engine, null), }; - internal static string ToDisplay(this BenchmarkIoMode mode) => mode switch + public static string ToDisplay(this BenchmarkIoMode mode) => mode switch { BenchmarkIoMode.ByteArray => "Byte[]", BenchmarkIoMode.BufWrReuse => "BufWr reuse", @@ -133,7 +133,7 @@ internal static class BenchmarkEnumExtensions _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null), }; - internal static string ToDisplay(this BenchmarkDispatchMode mode) => mode switch + public static string ToDisplay(this BenchmarkDispatchMode mode) => mode switch { BenchmarkDispatchMode.SGen => "SGen", BenchmarkDispatchMode.Runtime => "Runtime", diff --git a/AyCode.Benchmark/Workloads/Scenarios/BenchmarkOptions.cs b/AyCode.Benchmark/Workloads/Scenarios/BenchmarkOptions.cs new file mode 100644 index 0000000..05019ee --- /dev/null +++ b/AyCode.Benchmark/Workloads/Scenarios/BenchmarkOptions.cs @@ -0,0 +1,89 @@ +using AyCode.Core.Serializers; +using AyCode.Core.Serializers.Attributes; +using AyCode.Core.Serializers.Binaries; +using MemoryPack; +using System.Reflection; + +namespace AyCode.Core.Benchmarks.Workloads.Scenarios; + +/// +/// Per-engine options-formatting + selection helpers shared by all benchmark rows. Centralizes +/// the Options-column display string (so the .log / .LLM / console headers stay consistent), the +/// MemoryPack WireMode-aligned options selection (so AcBinary FastWire ↔ MemoryPack UTF-16 +/// comparisons stay apples-to-apples), and the cached attribute-flag aggregation. +/// +public static class BenchmarkOptions +{ + /// + /// Aggregated feature flags across every type tagged with + /// the attribute in the loaded assemblies. Cached on first access (single reflection scan at startup). + /// Used by so the per-row Options column shows BOTH the configured + /// options-level value AND the effective attribute-level enable flag — a feature flagged off at the + /// type level overrides the options regardless of preset, and that asymmetry must surface in the log + /// to avoid misreading a "RefHandling=OnlyId" / "Interning=All" line as actually active. + /// Aggregation rule: if ALL tagged types have the feature enabled → true; if any tagged type + /// disables it → false (a single disabling type suppresses the feature on the type-graph). + /// + public static readonly (bool refHandling, bool internString, bool metadata, bool idTracking, bool propertyFilter) AttrFlags + = ScanAttributeFlags(); + + private static (bool refHandling, bool internString, bool metadata, bool idTracking, bool propertyFilter) ScanAttributeFlags() + { + var attrs = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(a => { try { return a.GetTypes(); } catch { return Array.Empty(); } }) + .Select(t => t.GetCustomAttribute()) + .Where(a => a != null) + .ToList(); + + if (attrs.Count == 0) return (false, false, false, false, false); + + return ( + refHandling: attrs.All(a => a!.EnableRefHandlingFeature), + internString: attrs.All(a => a!.EnableInternStringFeature), + metadata: attrs.All(a => a!.EnableMetadataFeature), + idTracking: attrs.All(a => a!.EnableIdTrackingFeature), + propertyFilter: attrs.All(a => a!.EnablePropertyFilterFeature)); + } + + /// + /// Common Options-column formatter for every AcBinary serializer benchmark row. Renders the + /// configured options-level value AND the effective attribute-level enable flag side-by-side + /// (e.g. Interning=All(opt) | False (attr)) so attribute-suppressed features cannot + /// silently mislead. Pass any benchmark-specific extras (e.g. ", BufferSize=4096B") + /// in — they are appended after the common fields. + /// + public static string BuildAcBinary(AcBinarySerializerOptions options, string extra = "") + { + // PropertyFilter: opt-side is "Set"/"None" depending on whether a callback is registered (the callback + // itself isn't a meaningful display value); attr-side is the cross-type-aggregated bool (true = every + // tagged type has the feature enabled, false = at least one type opted out via + // [AcBinarySerializable(enablePropertyFilterFeature: false)] → SGen-emit + Runtime hot-loop both gate). + var propFilterOpt = options.PropertyFilter == null ? "None" : "Set"; + + return $"WireMode={options.WireMode}, " + + $"RefHandling={options.ReferenceHandling}(opt) | {AttrFlags.refHandling} (attr), " + + $"Interning={options.UseStringInterning}(opt) | {AttrFlags.internString} (attr), " + + $"Metadata={options.UseMetadata}(opt) | {AttrFlags.metadata} (attr), " + + $"PropertyFilter={propFilterOpt}(opt) | {AttrFlags.propertyFilter} (attr), " + + $"SGen={options.UseGeneratedCode}, " + + $"Compression={options.UseCompression}{extra}"; + } + + /// + /// Returns MemoryPack serializer options aligned with the given for a fair + /// apples-to-apples wire-format comparison: + /// + /// (UTF-8) — both + /// engines encode UTF-8, comparison is purely about header / tier / dispatch overhead. + /// (UTF-16 raw memcpy) — + /// both engines write UTF-16 raw bytes, so wire-size and CPU comparison reflect the same string-encoding family. + /// + /// Without this alignment the FastWire vs MemPack-default comparison conflates two unrelated dimensions + /// (UTF-16 raw vs UTF-8 encoded) and produces a misleading +40% wire-size delta that is structurally + /// the encoding-family difference, NOT an AcBinary-specific overhead. + /// + public static MemoryPackSerializerOptions GetMemPack(WireMode wireMode) => + wireMode == WireMode.Fast + ? MemoryPackSerializerOptions.Utf16 + : MemoryPackSerializerOptions.Default; +} diff --git a/AyCode.Core.Serializers.Console/Benchmarks/ISerializerBenchmark.cs b/AyCode.Benchmark/Workloads/Scenarios/ISerializerBenchmark.cs similarity index 96% rename from AyCode.Core.Serializers.Console/Benchmarks/ISerializerBenchmark.cs rename to AyCode.Benchmark/Workloads/Scenarios/ISerializerBenchmark.cs index 48aedcd..b6f8fad 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/ISerializerBenchmark.cs +++ b/AyCode.Benchmark/Workloads/Scenarios/ISerializerBenchmark.cs @@ -1,4 +1,4 @@ -namespace AyCode.Core.Serializers.Console.Benchmarks; +namespace AyCode.Core.Benchmarks.Workloads.Scenarios; /// /// Common contract for every per-engine, per-I/O-mode benchmark row. Each implementation captures @@ -11,7 +11,7 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// warmup state (rare). Round-trip-only benchmarks (NamedPipe etc.) set /// to true so the bench loop skips the Des-phase and routes timing into the RT columns. /// -internal interface ISerializerBenchmark +public interface ISerializerBenchmark { /// Serializer engine — typed enum, see for the human-readable form. BenchmarkEngine Engine { get; } @@ -24,7 +24,7 @@ internal interface ISerializerBenchmark /// /// CLR type of the order graph this benchmark serializes (e.g. typeof(TestOrder_All_False), /// typeof(TestOrder_All_True)). Per-instance: AcBinary picks variant by options preset - /// (), MemPack / MsgPack always use _All_False. + /// (caller-side dispatch rule), MemPack / MsgPack always use _All_False. /// Concrete benchmarks return typeof(T) for their generic parameter. /// Type OrderType { get; } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBenchmark.cs b/AyCode.Benchmark/Workloads/Scenarios/MemoryPackBenchmark.cs similarity index 80% rename from AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBenchmark.cs rename to AyCode.Benchmark/Workloads/Scenarios/MemoryPackBenchmark.cs index f270c28..b0713fe 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBenchmark.cs +++ b/AyCode.Benchmark/Workloads/Scenarios/MemoryPackBenchmark.cs @@ -1,15 +1,16 @@ -using AyCode.Core.Tests.TestModels; +using AyCode.Core.Serializers; +using AyCode.Core.Tests.TestModels; using MemoryPack; using System.Runtime.CompilerServices; -namespace AyCode.Core.Serializers.Console.Benchmarks; +namespace AyCode.Core.Benchmarks.Workloads.Scenarios; /// /// MemoryPack benchmark, Byte[] I/O mode. The SOTA baseline AcBinary is compared against in every /// cell. WireMode-aligned options via so Compact ↔ UTF-8 /// and FastWire ↔ UTF-16 are apples-to-apples on the string-encoding axis. /// -internal sealed class MemoryPackBenchmark : ISerializerBenchmark where T : class +public sealed class MemoryPackBenchmark : ISerializerBenchmark where T : class { private readonly T _order; private readonly MemoryPackSerializerOptions _options; @@ -25,11 +26,11 @@ internal sealed class MemoryPackBenchmark : ISerializerBenchmark where T : cl public long SetupDeserializeAllocBytes => 0; public string? OptionsDescription => $"StringEncoding={_options.StringEncoding}"; - public MemoryPackBenchmark(T order, string optionsPreset) + public MemoryPackBenchmark(T order, WireMode wireMode, string optionsPreset) { _order = order; OptionsPreset = optionsPreset; - _options = BenchmarkOptions.GetMemPack(); + _options = BenchmarkOptions.GetMemPack(wireMode); _serialized = MemoryPackSerializer.Serialize(order, _options); } @@ -43,6 +44,6 @@ internal sealed class MemoryPackBenchmark : ISerializerBenchmark where T : cl { var bytes = MemoryPackSerializer.Serialize(_order, _options); var roundTripped = MemoryPackSerializer.Deserialize(bytes, _options); - return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped); + return RoundTripValidator.DeepEqualsViaJson(_order, roundTripped); } } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBufferWriterBenchmark.cs b/AyCode.Benchmark/Workloads/Scenarios/MemoryPackBufferWriterBenchmark.cs similarity index 83% rename from AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBufferWriterBenchmark.cs rename to AyCode.Benchmark/Workloads/Scenarios/MemoryPackBufferWriterBenchmark.cs index bbcae0b..1ede08d 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackBufferWriterBenchmark.cs +++ b/AyCode.Benchmark/Workloads/Scenarios/MemoryPackBufferWriterBenchmark.cs @@ -1,16 +1,17 @@ -using AyCode.Core.Tests.TestModels; +using AyCode.Core.Serializers; +using AyCode.Core.Tests.TestModels; using MemoryPack; using System.Buffers; using System.Runtime.CompilerServices; -namespace AyCode.Core.Serializers.Console.Benchmarks; +namespace AyCode.Core.Benchmarks.Workloads.Scenarios; /// /// Benchmarks MemoryPack via the IBufferWriter overload with a pre-allocated, reused ArrayBufferWriter. -/// Apples-to-apples counterpart to — MemoryPack's IBufferWriter +/// Apples-to-apples counterpart to — MemoryPack's IBufferWriter /// is the path it's designed for. /// -internal sealed class MemoryPackBufferWriterBenchmark : ISerializerBenchmark where T : class +public sealed class MemoryPackBufferWriterBenchmark : ISerializerBenchmark where T : class { private readonly T _order; private readonly MemoryPackSerializerOptions _options; @@ -27,11 +28,11 @@ internal sealed class MemoryPackBufferWriterBenchmark : ISerializerBenchmark public long SetupDeserializeAllocBytes => 0; public string? OptionsDescription => $"StringEncoding={_options.StringEncoding}"; - public MemoryPackBufferWriterBenchmark(T order, string optionsPreset) + public MemoryPackBufferWriterBenchmark(T order, WireMode wireMode, string optionsPreset) { _order = order; OptionsPreset = optionsPreset; - _options = BenchmarkOptions.GetMemPack(); + _options = BenchmarkOptions.GetMemPack(wireMode); _serialized = MemoryPackSerializer.Serialize(order, _options); // Serialize-side setup only — see AcBinaryBufferWriterBenchmark for the full rationale. @@ -59,6 +60,6 @@ internal sealed class MemoryPackBufferWriterBenchmark : ISerializerBenchmark _bufferWriter.ResetWrittenCount(); MemoryPackSerializer.Serialize(_bufferWriter, _order, _options); var roundTripped = MemoryPackSerializer.Deserialize(new ReadOnlySequence(_bufferWriter.WrittenMemory), _options); - return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped); + return RoundTripValidator.DeepEqualsViaJson(_order, roundTripped); } } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackFreshBufferWriterBenchmark.cs b/AyCode.Benchmark/Workloads/Scenarios/MemoryPackFreshBufferWriterBenchmark.cs similarity index 80% rename from AyCode.Core.Serializers.Console/Benchmarks/MemoryPackFreshBufferWriterBenchmark.cs rename to AyCode.Benchmark/Workloads/Scenarios/MemoryPackFreshBufferWriterBenchmark.cs index 804ff63..6131801 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/MemoryPackFreshBufferWriterBenchmark.cs +++ b/AyCode.Benchmark/Workloads/Scenarios/MemoryPackFreshBufferWriterBenchmark.cs @@ -1,15 +1,16 @@ -using AyCode.Core.Tests.TestModels; +using AyCode.Core.Serializers; +using AyCode.Core.Tests.TestModels; using MemoryPack; using System.Buffers; using System.Runtime.CompilerServices; -namespace AyCode.Core.Serializers.Console.Benchmarks; +namespace AyCode.Core.Benchmarks.Workloads.Scenarios; /// /// Benchmarks MemoryPack via the IBufferWriter overload, allocating a FRESH ArrayBufferWriter on EVERY call. -/// Apples-to-apples counterpart to . +/// Apples-to-apples counterpart to . /// -internal sealed class MemoryPackFreshBufferWriterBenchmark : ISerializerBenchmark where T : class +public sealed class MemoryPackFreshBufferWriterBenchmark : ISerializerBenchmark where T : class { private readonly T _order; private readonly MemoryPackSerializerOptions _options; @@ -25,11 +26,11 @@ internal sealed class MemoryPackFreshBufferWriterBenchmark : ISerializerBench public long SetupDeserializeAllocBytes => 0; public string? OptionsDescription => $"StringEncoding={_options.StringEncoding}"; - public MemoryPackFreshBufferWriterBenchmark(T order, string optionsPreset) + public MemoryPackFreshBufferWriterBenchmark(T order, WireMode wireMode, string optionsPreset) { _order = order; OptionsPreset = optionsPreset; - _options = BenchmarkOptions.GetMemPack(); + _options = BenchmarkOptions.GetMemPack(wireMode); _serialized = MemoryPackSerializer.Serialize(order, _options); } @@ -50,6 +51,6 @@ internal sealed class MemoryPackFreshBufferWriterBenchmark : ISerializerBench var abw = new ArrayBufferWriter(); MemoryPackSerializer.Serialize(abw, _order, _options); var roundTripped = MemoryPackSerializer.Deserialize(new ReadOnlySequence(abw.WrittenMemory), _options); - return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped); + return RoundTripValidator.DeepEqualsViaJson(_order, roundTripped); } } diff --git a/AyCode.Core.Serializers.Console/Benchmarks/MessagePackBenchmark.cs b/AyCode.Benchmark/Workloads/Scenarios/MessagePackBenchmark.cs similarity index 91% rename from AyCode.Core.Serializers.Console/Benchmarks/MessagePackBenchmark.cs rename to AyCode.Benchmark/Workloads/Scenarios/MessagePackBenchmark.cs index 15583cc..1148db4 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/MessagePackBenchmark.cs +++ b/AyCode.Benchmark/Workloads/Scenarios/MessagePackBenchmark.cs @@ -1,10 +1,10 @@ -#if !AYCODE_NATIVEAOT +#if !AYCODE_NATIVEAOT using AyCode.Core.Tests.TestModels; using MessagePack; using MessagePack.Resolvers; using System.Runtime.CompilerServices; -namespace AyCode.Core.Serializers.Console.Benchmarks; +namespace AyCode.Core.Benchmarks.Workloads.Scenarios; /// /// MessagePack benchmark, Byte[] I/O mode. Excluded from NativeAOT build because v3's StandardResolver @@ -12,7 +12,7 @@ namespace AyCode.Core.Serializers.Console.Benchmarks; /// which uses Activator.CreateInstance on formatter types the AOT trimmer drops → /// MissingMethodException at runtime. Available for regular JIT runs (dotnet run) only. /// -internal sealed class MessagePackBenchmark : ISerializerBenchmark where T : class +public sealed class MessagePackBenchmark : ISerializerBenchmark where T : class { private readonly T _order; private readonly MessagePackSerializerOptions _options; @@ -53,7 +53,7 @@ internal sealed class MessagePackBenchmark : ISerializerBenchmark where T : c { var bytes = MessagePackSerializer.Serialize(_order, _options); var roundTripped = MessagePackSerializer.Deserialize(bytes, _options); - return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped); + return RoundTripValidator.DeepEqualsViaJson(_order, roundTripped); } } #endif diff --git a/AyCode.Benchmark/Workloads/Scenarios/RoundTripValidator.cs b/AyCode.Benchmark/Workloads/Scenarios/RoundTripValidator.cs new file mode 100644 index 0000000..cc81873 --- /dev/null +++ b/AyCode.Benchmark/Workloads/Scenarios/RoundTripValidator.cs @@ -0,0 +1,49 @@ +using System.Text.Json; + +namespace AyCode.Core.Benchmarks.Workloads.Scenarios; + +/// +/// Round-trip correctness validator — serializes both sides to canonical System.Text.Json form +/// and compares the resulting strings. Works for any object graph without a custom comparer (slower +/// than property-by-property but universal). Used by every benchmark's VerifyRoundTrip() +/// implementation as the deep-equality oracle before warmup begins. +/// +public static class RoundTripValidator +{ +#if !AYCODE_NATIVEAOT + private static readonly JsonSerializerOptions VerifyJsonOpts = new() + { + WriteIndented = false, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, + ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles + }; +#endif + + /// + /// Round-trip equality check via canonical System.Text.Json. Returns true if both sides serialize + /// to identical JSON strings. + /// + /// + /// AOT publish skip: System.Text.Json's reflection path uses runtime closed-generic instantiation + /// (JsonPropertyInfo<TestStatus> et al.) that the trimmer drops, causing + /// NotSupportedException: missing native code or metadata. The validation is JIT-only — the actual + /// benchmark Serialize/Deserialize loops don't touch this path. Under AOT we return true so all + /// VerifyRoundTrip() calls pass without running the cross-format validation. + /// + public static bool DeepEqualsViaJson(object? a, object? b) + { +#if AYCODE_NATIVEAOT + // Skip cross-format validation under AOT — STJ reflection path is incompatible. The roundtrip + // itself still runs (caller-side Serialize+Deserialize), just the JSON-canonical compare is bypassed. + return true; +#else + if (a == null && b == null) return true; + if (a == null || b == null) return false; + + var jsonA = JsonSerializer.Serialize(a, VerifyJsonOpts); + var jsonB = JsonSerializer.Serialize(b, VerifyJsonOpts); + + return jsonA == jsonB; +#endif + } +} diff --git a/AyCode.Core.Serializers.Console/Benchmarks/SystemTextJsonBenchmark.cs b/AyCode.Benchmark/Workloads/Scenarios/SystemTextJsonBenchmark.cs similarity index 72% rename from AyCode.Core.Serializers.Console/Benchmarks/SystemTextJsonBenchmark.cs rename to AyCode.Benchmark/Workloads/Scenarios/SystemTextJsonBenchmark.cs index 02debb9..7ae0f77 100644 --- a/AyCode.Core.Serializers.Console/Benchmarks/SystemTextJsonBenchmark.cs +++ b/AyCode.Benchmark/Workloads/Scenarios/SystemTextJsonBenchmark.cs @@ -1,16 +1,17 @@ -using AyCode.Core.Tests.TestModels; +using AyCode.Core.Tests.TestModels; using System.Runtime.CompilerServices; +using System.Text; using System.Text.Json; -namespace AyCode.Core.Serializers.Console.Benchmarks; +namespace AyCode.Core.Benchmarks.Workloads.Scenarios; /// /// System.Text.Json benchmark, String I/O mode. Reference comparison — uses reflection-based metadata -/// (no source-generator opt-in here). Typically NOT in the active suite (commented out in -/// BenchmarkLoop.CreateSerializers); ranks far behind binary serializers on µs/op but provides +/// (no source-generator opt-in here). Typically NOT in the active suite (commented out in the +/// caller-side CreateSerializers); ranks far behind binary serializers on µs/op but provides /// a familiar JSON baseline when needed. /// -internal sealed class SystemTextJsonBenchmark : ISerializerBenchmark where T : class +public sealed class SystemTextJsonBenchmark : ISerializerBenchmark where T : class { private readonly T _order; private readonly JsonSerializerOptions _options; @@ -37,7 +38,10 @@ internal sealed class SystemTextJsonBenchmark : ISerializerBenchmark where T ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles }; _serialized = JsonSerializer.Serialize(order, _options); - _serializedUtf8 = Configuration.Utf8NoBom.GetBytes(_serialized); + // Encoding.UTF8.GetBytes(string) does NOT prepend the BOM (the preamble is only emitted by + // GetPreamble() / stream-level writes), so this produces identical bytes to the prior + // `new UTF8Encoding(false).GetBytes(_serialized)` call. Size-reporting only. + _serializedUtf8 = Encoding.UTF8.GetBytes(_serialized); } [MethodImpl(MethodImplOptions.NoInlining)] @@ -50,6 +54,6 @@ internal sealed class SystemTextJsonBenchmark : ISerializerBenchmark where T { var json = JsonSerializer.Serialize(_order, _options); var roundTripped = JsonSerializer.Deserialize(json, _options); - return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped); + return RoundTripValidator.DeepEqualsViaJson(_order, roundTripped); } } diff --git a/AyCode.Core.Serializers.Console/AyCode.Core.Serializers.Console.csproj b/AyCode.Core.Serializers.Console/AyCode.Core.Serializers.Console.csproj index 4c4bf12..4f446a9 100644 --- a/AyCode.Core.Serializers.Console/AyCode.Core.Serializers.Console.csproj +++ b/AyCode.Core.Serializers.Console/AyCode.Core.Serializers.Console.csproj @@ -3,6 +3,7 @@ + @@ -16,6 +17,7 @@ net9.0 enable enable + AyCode.Core.Serializers.Console.Program