Refactor: move benchmark engines to shared scenarios
Major refactor: all benchmark engine implementations, enums, and helpers moved from Console project to AyCode.Core.Benchmarks.Workloads.Scenarios for unified cross-runner use. BenchmarkResult and reporting logic moved to AyCode.Core.Benchmarks.Reporting. Attribute-flag aggregation centralized in BenchmarkOptions. Updated all usages, project references, and SGen codegen for ref-handling. Prepares codebase for shared reporting and future extensibility.
This commit is contained in:
parent
d9ab3940eb
commit
9dcb62ce23
|
|
@ -103,7 +103,8 @@
|
||||||
"Bash(awk 'NR < 69 || NR > 1257 { print }' AcBinarySourceGenerator.cs)",
|
"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 == 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 < 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.\")"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
|
||||||
|
<PackageReference Include="MemoryPack" Version="1.21.4" />
|
||||||
<PackageReference Include="MessagePack" Version="3.1.4" />
|
<PackageReference Include="MessagePack" Version="3.1.4" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.DiagnosticsHub.BenchmarkDotNetDiagnosers" Version="18.6.37110.2" />
|
<PackageReference Include="Microsoft.VisualStudio.DiagnosticsHub.BenchmarkDotNetDiagnosers" Version="18.6.37110.2" />
|
||||||
<PackageReference Include="MongoDB.Bson" Version="3.5.2" />
|
<PackageReference Include="MongoDB.Bson" Version="3.5.2" />
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per-cell benchmark result row. Populated by the benchmark execution loop in
|
/// Per-cell benchmark result row. Populated by the benchmark execution loop (Console-side
|
||||||
/// <c>BenchmarkLoop.RunBenchmarksForTestData</c>; consumed by the output formatters in
|
/// <c>BenchmarkLoop.RunBenchmarksForTestData</c> / BDN-side <c>BdnSummaryAdapter</c>); consumed by the
|
||||||
/// <c>Output</c> (console table + .log + .LLM file writers). Pure DTO — no behaviour.
|
/// output formatters in <c>BenchmarkReportWriter</c> (console table + .log + .LLM file writers).
|
||||||
|
/// Pure DTO — no behaviour.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class BenchmarkResult
|
public sealed class BenchmarkResult
|
||||||
{
|
{
|
||||||
public string TestDataName { get; set; } = "";
|
public string TestDataName { get; set; } = "";
|
||||||
public BenchmarkEngine Engine { get; set; }
|
public BenchmarkEngine Engine { get; set; }
|
||||||
|
|
@ -18,7 +19,7 @@ internal sealed class BenchmarkResult
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// CLR type name of the order graph serialised in this row (e.g. <c>"TestOrder_All_False"</c>,
|
/// CLR type name of the order graph serialised in this row (e.g. <c>"TestOrder_All_False"</c>,
|
||||||
/// <c>"TestOrder_All_True"</c>). Captured from <see cref="ISerializerBenchmark.OrderTypeName"/> in
|
/// <c>"TestOrder_All_True"</c>). Captured from <see cref="ISerializerBenchmark.OrderTypeName"/> in
|
||||||
/// <c>RunBenchmarksForTestData</c>; 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
|
/// (.log, .LLM, console) so the reader can correlate each preset with its TestOrder variant
|
||||||
/// without inflating the per-row tables with an extra column.
|
/// without inflating the per-row tables with an extra column.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace AyCode.Core.Benchmarks.Reporting;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Context bundle for the unified benchmark report writer. Same record on both sides (Console / BDN),
|
||||||
|
/// only the <see cref="SourceTag"/> differs ("Console" / "Bdn") — that drives the filename prefix
|
||||||
|
/// (e.g. <c>Console.FullBenchmark_Release_{timestamp}.LLM</c> vs <c>Bdn.FullBenchmark_Release_{timestamp}.LLM</c>).
|
||||||
|
/// The <see cref="ResultsDirectory"/> resolution walks up from <see cref="AppContext.BaseDirectory"/> to the
|
||||||
|
/// nearest <c>AyCode.Core.sln</c> and combines with <c>Test_Benchmark_Results\Benchmark</c> — works across
|
||||||
|
/// build modes (Debug / Release / AOT publish) and worktrees (each worktree has its own .sln, so its bench
|
||||||
|
/// results land alongside its code).
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ReportingContext(
|
||||||
|
string SourceTag,
|
||||||
|
string ResultsDirectory,
|
||||||
|
string BuildConfiguration,
|
||||||
|
UTF8Encoding Utf8NoBom)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves the canonical <see cref="ResultsDirectory"/> by walking up from
|
||||||
|
/// <see cref="AppContext.BaseDirectory"/> to the nearest <c>AyCode.Core.sln</c>, then combining
|
||||||
|
/// with <c>Test_Benchmark_Results\Benchmark</c>. The build configuration is supplied by the caller
|
||||||
|
/// (Console resolves via <c>#if AYCODE_NATIVEAOT|DEBUG|else Release</c>; BDN-side currently mirrors
|
||||||
|
/// the JIT vs AOT discriminator from <see cref="System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeCompiled"/>).
|
||||||
|
/// </summary>
|
||||||
|
public static ReportingContext Create(string sourceTag, string buildConfiguration) =>
|
||||||
|
new(sourceTag, ResolveResultsDirectory(), buildConfiguration, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Walk-up from the assembly's BaseDirectory to find the repo root (marker: <c>AyCode.Core.sln</c>).
|
||||||
|
/// Returns <c>{repoRoot}\Test_Benchmark_Results\Benchmark</c>. 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.
|
||||||
|
/// </summary>
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
using AyCode.Core.Serializers.Binaries;
|
using AyCode.Core.Serializers.Binaries;
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// AcBinary benchmark, Byte[] I/O mode. The headline AcBinary row in every cell — compared
|
/// AcBinary benchmark, Byte[] I/O mode. The headline AcBinary row in every cell — compared
|
||||||
/// against <see cref="MemoryPackBenchmark"/> as the SOTA baseline.
|
/// against <see cref="MemoryPackBenchmark{T}"/> as the SOTA baseline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class AcBinaryBenchmark<T> : ISerializerBenchmark where T : class
|
public sealed class AcBinaryBenchmark<T> : ISerializerBenchmark where T : class
|
||||||
{
|
{
|
||||||
private readonly T _order;
|
private readonly T _order;
|
||||||
private readonly AcBinarySerializerOptions _options;
|
private readonly AcBinarySerializerOptions _options;
|
||||||
|
|
@ -42,6 +42,6 @@ internal sealed class AcBinaryBenchmark<T> : ISerializerBenchmark where T : clas
|
||||||
{
|
{
|
||||||
var bytes = AcBinarySerializer.Serialize(_order, _options);
|
var bytes = AcBinarySerializer.Serialize(_order, _options);
|
||||||
var roundTripped = AcBinaryDeserializer.Deserialize<T>(bytes, _options);
|
var roundTripped = AcBinaryDeserializer.Deserialize<T>(bytes, _options);
|
||||||
return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped);
|
return RoundTripValidator.DeepEqualsViaJson(_order, roundTripped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
using AyCode.Core.Serializers.Binaries;
|
using AyCode.Core.Serializers.Binaries;
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Benchmarks AcBinary via the IBufferWriter overload with a pre-allocated, reused ArrayBufferWriter.
|
/// 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).
|
/// Realistic IBufferWriter usage pattern: caller owns + reuses the writer (zero alloc per call after warmup).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class AcBinaryBufferWriterBenchmark<T> : ISerializerBenchmark where T : class
|
public sealed class AcBinaryBufferWriterBenchmark<T> : ISerializerBenchmark where T : class
|
||||||
{
|
{
|
||||||
private readonly T _order;
|
private readonly T _order;
|
||||||
private readonly AcBinarySerializerOptions _options;
|
private readonly AcBinarySerializerOptions _options;
|
||||||
|
|
@ -64,6 +64,6 @@ internal sealed class AcBinaryBufferWriterBenchmark<T> : ISerializerBenchmark wh
|
||||||
AcBinarySerializer.Serialize(_order, _bufferWriter, _options);
|
AcBinarySerializer.Serialize(_order, _bufferWriter, _options);
|
||||||
|
|
||||||
var roundTripped = AcBinaryDeserializer.Deserialize<T>(new ReadOnlySequence<byte>(_bufferWriter.WrittenMemory), _options);
|
var roundTripped = AcBinaryDeserializer.Deserialize<T>(new ReadOnlySequence<byte>(_bufferWriter.WrittenMemory), _options);
|
||||||
return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped);
|
return RoundTripValidator.DeepEqualsViaJson(_order, roundTripped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
using AyCode.Core.Serializers.Binaries;
|
using AyCode.Core.Serializers.Binaries;
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Benchmarks AcBinary via the IBufferWriter overload, allocating a FRESH ArrayBufferWriter on EVERY call.
|
/// 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
|
/// 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).
|
/// regardless of payload size (heavy over-allocation for small payloads).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class AcBinaryFreshBufferWriterBenchmark<T> : ISerializerBenchmark where T : class
|
public sealed class AcBinaryFreshBufferWriterBenchmark<T> : ISerializerBenchmark where T : class
|
||||||
{
|
{
|
||||||
private readonly T _order;
|
private readonly T _order;
|
||||||
private readonly AcBinarySerializerOptions _options;
|
private readonly AcBinarySerializerOptions _options;
|
||||||
|
|
@ -59,6 +59,6 @@ internal sealed class AcBinaryFreshBufferWriterBenchmark<T> : ISerializerBenchma
|
||||||
var abw = new ArrayBufferWriter<byte>();
|
var abw = new ArrayBufferWriter<byte>();
|
||||||
AcBinarySerializer.Serialize(_order, abw, _options);
|
AcBinarySerializer.Serialize(_order, abw, _options);
|
||||||
var roundTripped = AcBinaryDeserializer.Deserialize<T>(new ReadOnlySequence<byte>(abw.WrittenMemory), _options);
|
var roundTripped = AcBinaryDeserializer.Deserialize<T>(new ReadOnlySequence<byte>(abw.WrittenMemory), _options);
|
||||||
return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped);
|
return RoundTripValidator.DeepEqualsViaJson(_order, roundTripped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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.Serialization; // DrainFromAsync extension (test-only, used by benchmark)
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Same chunked-framed AsyncPipe code path as <see cref="AcBinaryNamedPipeBenchmark"/>, but the transport
|
/// Same chunked-framed AsyncPipe code path as <see cref="AcBinaryNamedPipeBenchmark{T}"/>, but the transport
|
||||||
/// is an in-memory <see cref="System.IO.Pipelines.Pipe"/> instead of a kernel <c>NamedPipe</c>. The Pipe's
|
/// is an in-memory <see cref="System.IO.Pipelines.Pipe"/> instead of a kernel <c>NamedPipe</c>. The Pipe's
|
||||||
/// <c>Writer</c>/<c>Reader</c> pair is a managed-only zero-copy slab handoff — no syscalls, no kernel
|
/// <c>Writer</c>/<c>Reader</c> pair is a managed-only zero-copy slab handoff — no syscalls, no kernel
|
||||||
/// buffer copy, no IRP queueing.
|
/// buffer copy, no IRP queueing.
|
||||||
|
|
@ -15,16 +15,16 @@ namespace AyCode.Core.Serializers.Console.Benchmarks;
|
||||||
/// <para><b>Why this benchmark matters</b>: by holding ALL other variables constant (same SerializeChunkedFramed,
|
/// <para><b>Why this benchmark matters</b>: by holding ALL other variables constant (same SerializeChunkedFramed,
|
||||||
/// same AsyncPipeReaderInput, same drain task, same consumer task, same multi-message wire format), this
|
/// same AsyncPipeReaderInput, same drain task, same consumer task, same multi-message wire format), this
|
||||||
/// row isolates the <b>kernel-NamedPipe transport overhead</b> from the chunked-streaming framework's pure
|
/// row isolates the <b>kernel-NamedPipe transport overhead</b> from the chunked-streaming framework's pure
|
||||||
/// CPU cost. The expected delta vs <see cref="AcBinaryNamedPipeBenchmark"/>: per-chunk overhead drops from
|
/// CPU cost. The expected delta vs <see cref="AcBinaryNamedPipeBenchmark{T}"/>: per-chunk overhead drops from
|
||||||
/// ~25-30 µs (kernel-syscall pair + IRP) to ~1-2 µs (managed slab handoff). Multi-chunk Large-message rows
|
/// ~25-30 µs (kernel-syscall pair + IRP) to ~1-2 µs (managed slab handoff). Multi-chunk Large-message rows
|
||||||
/// should converge dramatically toward <see cref="AcBinaryNamedPipeRawByteArrayBenchmark"/>.</para>
|
/// should converge dramatically toward <see cref="AcBinaryNamedPipeRawByteArrayBenchmark{T}"/>.</para>
|
||||||
///
|
///
|
||||||
/// <para><b>Real-world relevance</b>: in-memory Pipe is the typical primitive used for cross-thread serializer
|
/// <para><b>Real-world relevance</b>: 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,
|
/// 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
|
/// custom message brokers). The numbers from this row reflect that scenario, NOT the kernel-pipe loopback
|
||||||
/// of the NamedPipe benchmark.</para>
|
/// of the NamedPipe benchmark.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class AcBinaryInMemoryPipeBenchmark<T> : ISerializerBenchmark, IDisposable where T : class
|
public sealed class AcBinaryInMemoryPipeBenchmark<T> : ISerializerBenchmark, IDisposable where T : class
|
||||||
{
|
{
|
||||||
private readonly T _order;
|
private readonly T _order;
|
||||||
private readonly AcBinarySerializerOptions _options;
|
private readonly AcBinarySerializerOptions _options;
|
||||||
|
|
@ -160,7 +160,7 @@ internal sealed class AcBinaryInMemoryPipeBenchmark<T> : ISerializerBenchmark, I
|
||||||
{
|
{
|
||||||
Serialize();
|
Serialize();
|
||||||
var result = _lastResult as T;
|
var result = _lastResult as T;
|
||||||
return result != null && BenchmarkLoop.DeepEqualsViaJson(_order, result);
|
return result != null && RoundTripValidator.DeepEqualsViaJson(_order, result);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
using AyCode.Core.Serializers.Binaries;
|
using AyCode.Core.Serializers.Binaries;
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Raw <c>byte[]</c> over an in-memory cross-thread handoff — NO transport (no NamedPipe, no Pipe, no
|
/// Raw <c>byte[]</c> over an in-memory cross-thread handoff — NO transport (no NamedPipe, no Pipe, no
|
||||||
|
|
@ -11,17 +11,17 @@ namespace AyCode.Core.Serializers.Console.Benchmarks;
|
||||||
///
|
///
|
||||||
/// <para><b>Why this benchmark matters</b>: completes the 2x2 transport × wire-format matrix:</para>
|
/// <para><b>Why this benchmark matters</b>: completes the 2x2 transport × wire-format matrix:</para>
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item><description><b>NamedPipe + Chunked</b> = <see cref="AcBinaryNamedPipeBenchmark"/></description></item>
|
/// <item><description><b>NamedPipe + Chunked</b> = <see cref="AcBinaryNamedPipeBenchmark{T}"/></description></item>
|
||||||
/// <item><description><b>NamedPipe + Raw</b> = <see cref="AcBinaryNamedPipeRawByteArrayBenchmark"/></description></item>
|
/// <item><description><b>NamedPipe + Raw</b> = <see cref="AcBinaryNamedPipeRawByteArrayBenchmark{T}"/></description></item>
|
||||||
/// <item><description><b>In-memory Pipe + Chunked</b> = <see cref="AcBinaryInMemoryPipeBenchmark"/></description></item>
|
/// <item><description><b>In-memory Pipe + Chunked</b> = <see cref="AcBinaryInMemoryPipeBenchmark{T}"/></description></item>
|
||||||
/// <item><description><b>In-memory + Raw</b> = THIS row — apples-to-apples baseline for the in-memory chunked row</description></item>
|
/// <item><description><b>In-memory + Raw</b> = THIS row — apples-to-apples baseline for the in-memory chunked row</description></item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// <para>Side-by-side with <see cref="AcBinaryInMemoryPipeBenchmark"/> this isolates the chunked-streaming
|
/// <para>Side-by-side with <see cref="AcBinaryInMemoryPipeBenchmark{T}"/> this isolates the chunked-streaming
|
||||||
/// framework's pure CPU cost, with the same in-memory transport (zero kernel involvement) on both sides.
|
/// framework's pure CPU cost, with the same in-memory transport (zero kernel involvement) on both sides.
|
||||||
/// Side-by-side with <see cref="AcBinaryNamedPipeRawByteArrayBenchmark"/> this isolates the kernel-NamedPipe
|
/// Side-by-side with <see cref="AcBinaryNamedPipeRawByteArrayBenchmark{T}"/> this isolates the kernel-NamedPipe
|
||||||
/// overhead on the raw-byte[] side.</para>
|
/// overhead on the raw-byte[] side.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class AcBinaryInMemoryRawByteArrayBenchmark<T> : ISerializerBenchmark, IDisposable where T : class
|
public sealed class AcBinaryInMemoryRawByteArrayBenchmark<T> : ISerializerBenchmark, IDisposable where T : class
|
||||||
{
|
{
|
||||||
private readonly T _order;
|
private readonly T _order;
|
||||||
private readonly AcBinarySerializerOptions _options;
|
private readonly AcBinarySerializerOptions _options;
|
||||||
|
|
@ -144,7 +144,7 @@ internal sealed class AcBinaryInMemoryRawByteArrayBenchmark<T> : ISerializerBenc
|
||||||
{
|
{
|
||||||
Serialize();
|
Serialize();
|
||||||
var result = _lastResult as T;
|
var result = _lastResult as T;
|
||||||
return result != null && BenchmarkLoop.DeepEqualsViaJson(_order, result);
|
return result != null && RoundTripValidator.DeepEqualsViaJson(_order, result);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -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.Serialization; // DrainFromAsync extension (test-only, used by benchmark)
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
using System.IO.Pipes;
|
using System.IO.Pipes;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Benchmarks AcBinary over a long-lived NamedPipe IPC connection using the AcBinary native streaming API
|
/// 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;
|
||||||
/// <para><b>Approximation note</b>: single-process loopback NamedPipe. Real cross-process / cross-machine SignalR
|
/// <para><b>Approximation note</b>: 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.</para>
|
/// adds further transport latency (TCP, WebSocket framing) on top. The benchmark gives a lower bound.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class AcBinaryNamedPipeBenchmark<T> : ISerializerBenchmark, IDisposable where T : class
|
public sealed class AcBinaryNamedPipeBenchmark<T> : ISerializerBenchmark, IDisposable where T : class
|
||||||
{
|
{
|
||||||
private readonly T _order;
|
private readonly T _order;
|
||||||
private readonly AcBinarySerializerOptions _options;
|
private readonly AcBinarySerializerOptions _options;
|
||||||
|
|
@ -205,7 +205,7 @@ internal sealed class AcBinaryNamedPipeBenchmark<T> : ISerializerBenchmark, IDis
|
||||||
{
|
{
|
||||||
Serialize();
|
Serialize();
|
||||||
var result = _lastResult as T;
|
var result = _lastResult as T;
|
||||||
return result != null && BenchmarkLoop.DeepEqualsViaJson(_order, result);
|
return result != null && RoundTripValidator.DeepEqualsViaJson(_order, result);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
using AyCode.Core.Serializers.Binaries;
|
using AyCode.Core.Serializers.Binaries;
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using System.IO.Pipes;
|
using System.IO.Pipes;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Raw <c>byte[]</c> over a long-lived NamedPipe — NO chunk-framing, NO <c>AsyncPipeReaderInput</c>,
|
/// Raw <c>byte[]</c> over a long-lived NamedPipe — NO chunk-framing, NO <c>AsyncPipeReaderInput</c>,
|
||||||
|
|
@ -11,21 +11,21 @@ namespace AyCode.Core.Serializers.Console.Benchmarks;
|
||||||
/// reads and deserialises. Two-task pattern enables Ser↔Read overlap (kernel-pipe-pipelined) AND
|
/// reads and deserialises. Two-task pattern enables Ser↔Read overlap (kernel-pipe-pipelined) AND
|
||||||
/// avoids the kernel-buffer-full deadlock when <c>bytes.Length > inBufferSize</c>.
|
/// avoids the kernel-buffer-full deadlock when <c>bytes.Length > inBufferSize</c>.
|
||||||
///
|
///
|
||||||
/// Side-by-side with <see cref="AcBinaryNamedPipeBenchmark"/> (chunked-framed AsyncPipe stack) this
|
/// Side-by-side with <see cref="AcBinaryNamedPipeBenchmark{T}"/> (chunked-framed AsyncPipe stack) this
|
||||||
/// isolates two cost components on the SAME kernel-pipe transport with the SAME <c>inBufferSize</c>:
|
/// isolates two cost components on the SAME kernel-pipe transport with the SAME <c>inBufferSize</c>:
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item><description><b>This row vs <see cref="AcBinaryBenchmark"/> (Byte[])</b> — pure kernel-NamedPipe
|
/// <item><description><b>This row vs <see cref="AcBinaryBenchmark{T}"/> (Byte[])</b> — pure kernel-NamedPipe
|
||||||
/// overhead (WriteFile / ReadFile syscalls + IRP queueing + buffer-copy + thread-handoff).</description></item>
|
/// overhead (WriteFile / ReadFile syscalls + IRP queueing + buffer-copy + thread-handoff).</description></item>
|
||||||
/// <item><description><b>This row vs <see cref="AcBinaryNamedPipeBenchmark"/> (chunked-framed)</b> — pure
|
/// <item><description><b>This row vs <see cref="AcBinaryNamedPipeBenchmark{T}"/> (chunked-framed)</b> — pure
|
||||||
/// AsyncPipe-framework overhead (chunk header writes + sliding-window <c>Feed</c> + MRES wait inside
|
/// AsyncPipe-framework overhead (chunk header writes + sliding-window <c>Feed</c> + MRES wait inside
|
||||||
/// <c>AsyncPipeReaderInput</c>) AND the streaming-pipeline benefit of intra-message Ser↔Des overlap (which
|
/// <c>AsyncPipeReaderInput</c>) 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).</description></item>
|
/// raw lacks — raw can only Ser↔Read overlap, with Des sequential after Read completes).</description></item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// Per-iter <c>byte[]</c> allocation from <c>AcBinarySerializer.Serialize</c> is part of the cost (matches
|
/// Per-iter <c>byte[]</c> allocation from <c>AcBinarySerializer.Serialize</c> is part of the cost (matches
|
||||||
/// <see cref="AcBinaryBenchmark"/>'s API contract); the receive-side scratch buffer is also allocated per-iter
|
/// <see cref="AcBinaryBenchmark{T}"/>'s API contract); the receive-side scratch buffer is also allocated per-iter
|
||||||
/// on the consumer-task (counted via <c>GC.GetTotalAllocatedBytes</c> in <c>BenchmarkLoop.MeasureAllocationTotal</c>).
|
/// on the consumer-task (counted via <c>GC.GetTotalAllocatedBytes</c> in <c>BenchmarkLoop.MeasureAllocationTotal</c>).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class AcBinaryNamedPipeRawByteArrayBenchmark<T> : ISerializerBenchmark, IDisposable where T : class
|
public sealed class AcBinaryNamedPipeRawByteArrayBenchmark<T> : ISerializerBenchmark, IDisposable where T : class
|
||||||
{
|
{
|
||||||
private readonly T _order;
|
private readonly T _order;
|
||||||
private readonly AcBinarySerializerOptions _options;
|
private readonly AcBinarySerializerOptions _options;
|
||||||
|
|
@ -185,7 +185,7 @@ internal sealed class AcBinaryNamedPipeRawByteArrayBenchmark<T> : ISerializerBen
|
||||||
{
|
{
|
||||||
Serialize();
|
Serialize();
|
||||||
var result = _lastResult as T;
|
var result = _lastResult as T;
|
||||||
return result != null && BenchmarkLoop.DeepEqualsViaJson(_order, result);
|
return result != null && RoundTripValidator.DeepEqualsViaJson(_order, result);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Serializer engine identifier — replaces the prior <c>Configuration.EngineXxx</c> string constants
|
/// Serializer engine identifier — replaces the prior <c>Configuration.EngineXxx</c> string constants
|
||||||
/// with a type-safe enum. The benchmark-result <c>Engine</c> column uses <see cref="ToDisplay"/> for
|
/// with a type-safe enum. The benchmark-result <c>Engine</c> column uses <see cref="ToDisplay"/> for
|
||||||
/// the human-readable form.
|
/// the human-readable form.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal enum BenchmarkEngine
|
public enum BenchmarkEngine
|
||||||
{
|
{
|
||||||
AcBinary,
|
AcBinary,
|
||||||
MemoryPack,
|
MemoryPack,
|
||||||
|
|
@ -21,7 +21,7 @@ internal enum BenchmarkEngine
|
||||||
/// (they distinguish chunked-framed vs raw-byte[] semantics, but render identically in the IO column);
|
/// (they distinguish chunked-framed vs raw-byte[] semantics, but render identically in the IO column);
|
||||||
/// the same applies to <see cref="InMemoryPipe"/> + <see cref="InMemoryRaw"/> (<c>"Pipe(in-mem)"</c>).
|
/// the same applies to <see cref="InMemoryPipe"/> + <see cref="InMemoryRaw"/> (<c>"Pipe(in-mem)"</c>).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal enum BenchmarkIoMode
|
public enum BenchmarkIoMode
|
||||||
{
|
{
|
||||||
ByteArray,
|
ByteArray,
|
||||||
BufWrReuse,
|
BufWrReuse,
|
||||||
|
|
@ -42,7 +42,7 @@ internal enum BenchmarkIoMode
|
||||||
/// <item><see cref="Hybrid"/> — SGen root with non-SGen child types reached via bridge methods (see docs/BINARY/BINARY_SGEN.md).</item>
|
/// <item><see cref="Hybrid"/> — SGen root with non-SGen child types reached via bridge methods (see docs/BINARY/BINARY_SGEN.md).</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal enum BenchmarkDispatchMode
|
public enum BenchmarkDispatchMode
|
||||||
{
|
{
|
||||||
SGen,
|
SGen,
|
||||||
Runtime,
|
Runtime,
|
||||||
|
|
@ -50,7 +50,7 @@ internal enum BenchmarkDispatchMode
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Test-data layer filter — selects which <see cref="TestDataSet"/> cells participate in the run.
|
/// Test-data layer filter — selects which test data cells participate in the run.
|
||||||
/// Replaces the prior string-typed <c>layer</c> parameter; CLI/menu callers parse user input via
|
/// Replaces the prior string-typed <c>layer</c> parameter; CLI/menu callers parse user input via
|
||||||
/// <see cref="Enum.TryParse{T}(string, bool, out T)"/> with <c>ignoreCase: true</c>.
|
/// <see cref="Enum.TryParse{T}(string, bool, out T)"/> with <c>ignoreCase: true</c>.
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
|
|
@ -59,7 +59,7 @@ internal enum BenchmarkDispatchMode
|
||||||
/// <item><see cref="Small"/>/<see cref="Medium"/>/<see cref="Large"/>/<see cref="Repeated"/>/<see cref="Deep"/> — single-cell mini-suites for tight A/B iteration loops.</item>
|
/// <item><see cref="Small"/>/<see cref="Medium"/>/<see cref="Large"/>/<see cref="Repeated"/>/<see cref="Deep"/> — single-cell mini-suites for tight A/B iteration loops.</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal enum BenchmarkLayer
|
public enum BenchmarkLayer
|
||||||
{
|
{
|
||||||
All,
|
All,
|
||||||
Core,
|
Core,
|
||||||
|
|
@ -78,7 +78,7 @@ internal enum BenchmarkLayer
|
||||||
/// a no-op and only run on <see cref="Serialize"/> or <see cref="All"/>. Replaces the prior string-typed
|
/// a no-op and only run on <see cref="Serialize"/> or <see cref="All"/>. Replaces the prior string-typed
|
||||||
/// <c>mode</c>/<c>opMode</c> parameter.
|
/// <c>mode</c>/<c>opMode</c> parameter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal enum BenchmarkOpMode
|
public enum BenchmarkOpMode
|
||||||
{
|
{
|
||||||
All,
|
All,
|
||||||
Serialize,
|
Serialize,
|
||||||
|
|
@ -86,7 +86,7 @@ internal enum BenchmarkOpMode
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Serializer-set selection — drives <c>BenchmarkLoop.CreateSerializers</c> 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 <c>serializerMode</c>
|
/// preset bundles instead of a magic string. Replaces the prior string-typed <c>serializerMode</c>
|
||||||
/// parameter.
|
/// parameter.
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
|
|
@ -95,7 +95,7 @@ internal enum BenchmarkOpMode
|
||||||
/// <item><see cref="AsyncPipe"/> — streaming I/O isolation (NamedPipe + in-memory Pipe variants only).</item>
|
/// <item><see cref="AsyncPipe"/> — streaming I/O isolation (NamedPipe + in-memory Pipe variants only).</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal enum SerializerSelectionMode
|
public enum SerializerSelectionMode
|
||||||
{
|
{
|
||||||
Standard,
|
Standard,
|
||||||
FastestByte,
|
FastestByte,
|
||||||
|
|
@ -107,9 +107,9 @@ internal enum SerializerSelectionMode
|
||||||
/// human-readable form used by the per-row console table, the <c>.log</c> file CSV/formatted output,
|
/// human-readable form used by the per-row console table, the <c>.log</c> file CSV/formatted output,
|
||||||
/// and the <c>.LLM</c> markdown table. Centralised here so every output formatter renders identically.
|
/// and the <c>.LLM</c> markdown table. Centralised here so every output formatter renders identically.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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.AcBinary => "AcBinary",
|
||||||
BenchmarkEngine.MemoryPack => "MemoryPack",
|
BenchmarkEngine.MemoryPack => "MemoryPack",
|
||||||
|
|
@ -120,7 +120,7 @@ internal static class BenchmarkEnumExtensions
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(engine), engine, null),
|
_ => 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.ByteArray => "Byte[]",
|
||||||
BenchmarkIoMode.BufWrReuse => "BufWr reuse",
|
BenchmarkIoMode.BufWrReuse => "BufWr reuse",
|
||||||
|
|
@ -133,7 +133,7 @@ internal static class BenchmarkEnumExtensions
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
_ => 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.SGen => "SGen",
|
||||||
BenchmarkDispatchMode.Runtime => "Runtime",
|
BenchmarkDispatchMode.Runtime => "Runtime",
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 <c>WireMode</c>-aligned options selection (so AcBinary FastWire ↔ MemoryPack UTF-16
|
||||||
|
/// comparisons stay apples-to-apples), and the cached <see cref="AttrFlags"/> attribute-flag aggregation.
|
||||||
|
/// </summary>
|
||||||
|
public static class BenchmarkOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Aggregated <see cref="AcBinarySerializableAttribute"/> feature flags across every type tagged with
|
||||||
|
/// the attribute in the loaded assemblies. Cached on first access (single reflection scan at startup).
|
||||||
|
/// Used by <see cref="BuildAcBinary"/> 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 → <c>true</c>; if any tagged type
|
||||||
|
/// disables it → <c>false</c> (a single disabling type suppresses the feature on the type-graph).
|
||||||
|
/// </summary>
|
||||||
|
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<Type>(); } })
|
||||||
|
.Select(t => t.GetCustomAttribute<AcBinarySerializableAttribute>())
|
||||||
|
.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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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. <c>Interning=All(opt) | False (attr)</c>) so attribute-suppressed features cannot
|
||||||
|
/// silently mislead. Pass any benchmark-specific extras (e.g. <c>", BufferSize=4096B"</c>)
|
||||||
|
/// in <paramref name="extra"/> — they are appended after the common fields.
|
||||||
|
/// </summary>
|
||||||
|
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}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns MemoryPack serializer options aligned with the given <paramref name="wireMode"/> for a fair
|
||||||
|
/// apples-to-apples wire-format comparison:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><see cref="WireMode.Compact"/> → <see cref="MemoryPackSerializerOptions.Default"/> (UTF-8) — both
|
||||||
|
/// engines encode UTF-8, comparison is purely about header / tier / dispatch overhead.</item>
|
||||||
|
/// <item><see cref="WireMode.Fast"/> → <see cref="MemoryPackSerializerOptions.Utf16"/> (UTF-16 raw memcpy) —
|
||||||
|
/// both engines write UTF-16 raw bytes, so wire-size and CPU comparison reflect the same string-encoding family.</item>
|
||||||
|
/// </list>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public static MemoryPackSerializerOptions GetMemPack(WireMode wireMode) =>
|
||||||
|
wireMode == WireMode.Fast
|
||||||
|
? MemoryPackSerializerOptions.Utf16
|
||||||
|
: MemoryPackSerializerOptions.Default;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Common contract for every per-engine, per-I/O-mode benchmark row. Each implementation captures
|
/// 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 <see cref="IsRoundTripOnly"/>
|
/// warmup state (rare). Round-trip-only benchmarks (NamedPipe etc.) set <see cref="IsRoundTripOnly"/>
|
||||||
/// to true so the bench loop skips the Des-phase and routes timing into the RT columns.</para>
|
/// to true so the bench loop skips the Des-phase and routes timing into the RT columns.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal interface ISerializerBenchmark
|
public interface ISerializerBenchmark
|
||||||
{
|
{
|
||||||
/// <summary>Serializer engine — typed enum, see <see cref="BenchmarkEnumExtensions.ToDisplay(BenchmarkEngine)"/> for the human-readable form.</summary>
|
/// <summary>Serializer engine — typed enum, see <see cref="BenchmarkEnumExtensions.ToDisplay(BenchmarkEngine)"/> for the human-readable form.</summary>
|
||||||
BenchmarkEngine Engine { get; }
|
BenchmarkEngine Engine { get; }
|
||||||
|
|
@ -24,7 +24,7 @@ internal interface ISerializerBenchmark
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// CLR type of the order graph this benchmark serializes (e.g. <c>typeof(TestOrder_All_False)</c>,
|
/// CLR type of the order graph this benchmark serializes (e.g. <c>typeof(TestOrder_All_False)</c>,
|
||||||
/// <c>typeof(TestOrder_All_True)</c>). Per-instance: AcBinary picks variant by options preset
|
/// <c>typeof(TestOrder_All_True)</c>). Per-instance: AcBinary picks variant by options preset
|
||||||
/// (<see cref="BenchmarkLoop.UsesAllFalseVariant"/>), MemPack / MsgPack always use <c>_All_False</c>.
|
/// (caller-side dispatch rule), MemPack / MsgPack always use <c>_All_False</c>.
|
||||||
/// Concrete benchmarks return <c>typeof(T)</c> for their generic parameter.
|
/// Concrete benchmarks return <c>typeof(T)</c> for their generic parameter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Type OrderType { get; }
|
Type OrderType { get; }
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Serializers;
|
||||||
|
using AyCode.Core.Tests.TestModels;
|
||||||
using MemoryPack;
|
using MemoryPack;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MemoryPack benchmark, Byte[] I/O mode. The SOTA baseline AcBinary is compared against in every
|
/// MemoryPack benchmark, Byte[] I/O mode. The SOTA baseline AcBinary is compared against in every
|
||||||
/// cell. WireMode-aligned options via <see cref="BenchmarkOptions.GetMemPack"/> so Compact ↔ UTF-8
|
/// cell. WireMode-aligned options via <see cref="BenchmarkOptions.GetMemPack"/> so Compact ↔ UTF-8
|
||||||
/// and FastWire ↔ UTF-16 are apples-to-apples on the string-encoding axis.
|
/// and FastWire ↔ UTF-16 are apples-to-apples on the string-encoding axis.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class MemoryPackBenchmark<T> : ISerializerBenchmark where T : class
|
public sealed class MemoryPackBenchmark<T> : ISerializerBenchmark where T : class
|
||||||
{
|
{
|
||||||
private readonly T _order;
|
private readonly T _order;
|
||||||
private readonly MemoryPackSerializerOptions _options;
|
private readonly MemoryPackSerializerOptions _options;
|
||||||
|
|
@ -25,11 +26,11 @@ internal sealed class MemoryPackBenchmark<T> : ISerializerBenchmark where T : cl
|
||||||
public long SetupDeserializeAllocBytes => 0;
|
public long SetupDeserializeAllocBytes => 0;
|
||||||
public string? OptionsDescription => $"StringEncoding={_options.StringEncoding}";
|
public string? OptionsDescription => $"StringEncoding={_options.StringEncoding}";
|
||||||
|
|
||||||
public MemoryPackBenchmark(T order, string optionsPreset)
|
public MemoryPackBenchmark(T order, WireMode wireMode, string optionsPreset)
|
||||||
{
|
{
|
||||||
_order = order;
|
_order = order;
|
||||||
OptionsPreset = optionsPreset;
|
OptionsPreset = optionsPreset;
|
||||||
_options = BenchmarkOptions.GetMemPack();
|
_options = BenchmarkOptions.GetMemPack(wireMode);
|
||||||
_serialized = MemoryPackSerializer.Serialize(order, _options);
|
_serialized = MemoryPackSerializer.Serialize(order, _options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,6 +44,6 @@ internal sealed class MemoryPackBenchmark<T> : ISerializerBenchmark where T : cl
|
||||||
{
|
{
|
||||||
var bytes = MemoryPackSerializer.Serialize(_order, _options);
|
var bytes = MemoryPackSerializer.Serialize(_order, _options);
|
||||||
var roundTripped = MemoryPackSerializer.Deserialize<T>(bytes, _options);
|
var roundTripped = MemoryPackSerializer.Deserialize<T>(bytes, _options);
|
||||||
return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped);
|
return RoundTripValidator.DeepEqualsViaJson(_order, roundTripped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Serializers;
|
||||||
|
using AyCode.Core.Tests.TestModels;
|
||||||
using MemoryPack;
|
using MemoryPack;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Benchmarks MemoryPack via the IBufferWriter overload with a pre-allocated, reused ArrayBufferWriter.
|
/// Benchmarks MemoryPack via the IBufferWriter overload with a pre-allocated, reused ArrayBufferWriter.
|
||||||
/// Apples-to-apples counterpart to <see cref="AcBinaryBufferWriterBenchmark"/> — MemoryPack's IBufferWriter
|
/// Apples-to-apples counterpart to <see cref="AcBinaryBufferWriterBenchmark{T}"/> — MemoryPack's IBufferWriter
|
||||||
/// is the path it's designed for.
|
/// is the path it's designed for.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class MemoryPackBufferWriterBenchmark<T> : ISerializerBenchmark where T : class
|
public sealed class MemoryPackBufferWriterBenchmark<T> : ISerializerBenchmark where T : class
|
||||||
{
|
{
|
||||||
private readonly T _order;
|
private readonly T _order;
|
||||||
private readonly MemoryPackSerializerOptions _options;
|
private readonly MemoryPackSerializerOptions _options;
|
||||||
|
|
@ -27,11 +28,11 @@ internal sealed class MemoryPackBufferWriterBenchmark<T> : ISerializerBenchmark
|
||||||
public long SetupDeserializeAllocBytes => 0;
|
public long SetupDeserializeAllocBytes => 0;
|
||||||
public string? OptionsDescription => $"StringEncoding={_options.StringEncoding}";
|
public string? OptionsDescription => $"StringEncoding={_options.StringEncoding}";
|
||||||
|
|
||||||
public MemoryPackBufferWriterBenchmark(T order, string optionsPreset)
|
public MemoryPackBufferWriterBenchmark(T order, WireMode wireMode, string optionsPreset)
|
||||||
{
|
{
|
||||||
_order = order;
|
_order = order;
|
||||||
OptionsPreset = optionsPreset;
|
OptionsPreset = optionsPreset;
|
||||||
_options = BenchmarkOptions.GetMemPack();
|
_options = BenchmarkOptions.GetMemPack(wireMode);
|
||||||
_serialized = MemoryPackSerializer.Serialize(order, _options);
|
_serialized = MemoryPackSerializer.Serialize(order, _options);
|
||||||
|
|
||||||
// Serialize-side setup only — see AcBinaryBufferWriterBenchmark for the full rationale.
|
// Serialize-side setup only — see AcBinaryBufferWriterBenchmark for the full rationale.
|
||||||
|
|
@ -59,6 +60,6 @@ internal sealed class MemoryPackBufferWriterBenchmark<T> : ISerializerBenchmark
|
||||||
_bufferWriter.ResetWrittenCount();
|
_bufferWriter.ResetWrittenCount();
|
||||||
MemoryPackSerializer.Serialize(_bufferWriter, _order, _options);
|
MemoryPackSerializer.Serialize(_bufferWriter, _order, _options);
|
||||||
var roundTripped = MemoryPackSerializer.Deserialize<T>(new ReadOnlySequence<byte>(_bufferWriter.WrittenMemory), _options);
|
var roundTripped = MemoryPackSerializer.Deserialize<T>(new ReadOnlySequence<byte>(_bufferWriter.WrittenMemory), _options);
|
||||||
return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped);
|
return RoundTripValidator.DeepEqualsViaJson(_order, roundTripped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Serializers;
|
||||||
|
using AyCode.Core.Tests.TestModels;
|
||||||
using MemoryPack;
|
using MemoryPack;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Benchmarks MemoryPack via the IBufferWriter overload, allocating a FRESH ArrayBufferWriter on EVERY call.
|
/// Benchmarks MemoryPack via the IBufferWriter overload, allocating a FRESH ArrayBufferWriter on EVERY call.
|
||||||
/// Apples-to-apples counterpart to <see cref="AcBinaryFreshBufferWriterBenchmark"/>.
|
/// Apples-to-apples counterpart to <see cref="AcBinaryFreshBufferWriterBenchmark{T}"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class MemoryPackFreshBufferWriterBenchmark<T> : ISerializerBenchmark where T : class
|
public sealed class MemoryPackFreshBufferWriterBenchmark<T> : ISerializerBenchmark where T : class
|
||||||
{
|
{
|
||||||
private readonly T _order;
|
private readonly T _order;
|
||||||
private readonly MemoryPackSerializerOptions _options;
|
private readonly MemoryPackSerializerOptions _options;
|
||||||
|
|
@ -25,11 +26,11 @@ internal sealed class MemoryPackFreshBufferWriterBenchmark<T> : ISerializerBench
|
||||||
public long SetupDeserializeAllocBytes => 0;
|
public long SetupDeserializeAllocBytes => 0;
|
||||||
public string? OptionsDescription => $"StringEncoding={_options.StringEncoding}";
|
public string? OptionsDescription => $"StringEncoding={_options.StringEncoding}";
|
||||||
|
|
||||||
public MemoryPackFreshBufferWriterBenchmark(T order, string optionsPreset)
|
public MemoryPackFreshBufferWriterBenchmark(T order, WireMode wireMode, string optionsPreset)
|
||||||
{
|
{
|
||||||
_order = order;
|
_order = order;
|
||||||
OptionsPreset = optionsPreset;
|
OptionsPreset = optionsPreset;
|
||||||
_options = BenchmarkOptions.GetMemPack();
|
_options = BenchmarkOptions.GetMemPack(wireMode);
|
||||||
_serialized = MemoryPackSerializer.Serialize(order, _options);
|
_serialized = MemoryPackSerializer.Serialize(order, _options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,6 +51,6 @@ internal sealed class MemoryPackFreshBufferWriterBenchmark<T> : ISerializerBench
|
||||||
var abw = new ArrayBufferWriter<byte>();
|
var abw = new ArrayBufferWriter<byte>();
|
||||||
MemoryPackSerializer.Serialize(abw, _order, _options);
|
MemoryPackSerializer.Serialize(abw, _order, _options);
|
||||||
var roundTripped = MemoryPackSerializer.Deserialize<T>(new ReadOnlySequence<byte>(abw.WrittenMemory), _options);
|
var roundTripped = MemoryPackSerializer.Deserialize<T>(new ReadOnlySequence<byte>(abw.WrittenMemory), _options);
|
||||||
return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped);
|
return RoundTripValidator.DeepEqualsViaJson(_order, roundTripped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
#if !AYCODE_NATIVEAOT
|
#if !AYCODE_NATIVEAOT
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
using MessagePack.Resolvers;
|
using MessagePack.Resolvers;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MessagePack benchmark, Byte[] I/O mode. Excluded from NativeAOT build because v3's StandardResolver
|
/// 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 →
|
/// which uses Activator.CreateInstance on formatter types the AOT trimmer drops →
|
||||||
/// MissingMethodException at runtime. Available for regular JIT runs (<c>dotnet run</c>) only.
|
/// MissingMethodException at runtime. Available for regular JIT runs (<c>dotnet run</c>) only.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class MessagePackBenchmark<T> : ISerializerBenchmark where T : class
|
public sealed class MessagePackBenchmark<T> : ISerializerBenchmark where T : class
|
||||||
{
|
{
|
||||||
private readonly T _order;
|
private readonly T _order;
|
||||||
private readonly MessagePackSerializerOptions _options;
|
private readonly MessagePackSerializerOptions _options;
|
||||||
|
|
@ -53,7 +53,7 @@ internal sealed class MessagePackBenchmark<T> : ISerializerBenchmark where T : c
|
||||||
{
|
{
|
||||||
var bytes = MessagePackSerializer.Serialize(_order, _options);
|
var bytes = MessagePackSerializer.Serialize(_order, _options);
|
||||||
var roundTripped = MessagePackSerializer.Deserialize<T>(bytes, _options);
|
var roundTripped = MessagePackSerializer.Deserialize<T>(bytes, _options);
|
||||||
return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped);
|
return RoundTripValidator.DeepEqualsViaJson(_order, roundTripped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 <c>VerifyRoundTrip()</c>
|
||||||
|
/// implementation as the deep-equality oracle before warmup begins.
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round-trip equality check via canonical System.Text.Json. Returns true if both sides serialize
|
||||||
|
/// to identical JSON strings.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// AOT publish skip: <c>System.Text.Json</c>'s reflection path uses runtime closed-generic instantiation
|
||||||
|
/// (<c>JsonPropertyInfo<TestStatus></c> et al.) that the trimmer drops, causing
|
||||||
|
/// <c>NotSupportedException: missing native code or metadata</c>. The validation is JIT-only — the actual
|
||||||
|
/// benchmark Serialize/Deserialize loops don't touch this path. Under AOT we return <c>true</c> so all
|
||||||
|
/// <c>VerifyRoundTrip()</c> calls pass without running the cross-format validation.
|
||||||
|
/// </remarks>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// System.Text.Json benchmark, String I/O mode. Reference comparison — uses reflection-based metadata
|
/// 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
|
/// (no source-generator opt-in here). Typically NOT in the active suite (commented out in the
|
||||||
/// <c>BenchmarkLoop.CreateSerializers</c>); ranks far behind binary serializers on µs/op but provides
|
/// caller-side <c>CreateSerializers</c>); ranks far behind binary serializers on µs/op but provides
|
||||||
/// a familiar JSON baseline when needed.
|
/// a familiar JSON baseline when needed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class SystemTextJsonBenchmark<T> : ISerializerBenchmark where T : class
|
public sealed class SystemTextJsonBenchmark<T> : ISerializerBenchmark where T : class
|
||||||
{
|
{
|
||||||
private readonly T _order;
|
private readonly T _order;
|
||||||
private readonly JsonSerializerOptions _options;
|
private readonly JsonSerializerOptions _options;
|
||||||
|
|
@ -37,7 +38,10 @@ internal sealed class SystemTextJsonBenchmark<T> : ISerializerBenchmark where T
|
||||||
ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles
|
ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles
|
||||||
};
|
};
|
||||||
_serialized = JsonSerializer.Serialize(order, _options);
|
_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)]
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
|
@ -50,6 +54,6 @@ internal sealed class SystemTextJsonBenchmark<T> : ISerializerBenchmark where T
|
||||||
{
|
{
|
||||||
var json = JsonSerializer.Serialize(_order, _options);
|
var json = JsonSerializer.Serialize(_order, _options);
|
||||||
var roundTripped = JsonSerializer.Deserialize<T>(json, _options);
|
var roundTripped = JsonSerializer.Deserialize<T>(json, _options);
|
||||||
return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped);
|
return RoundTripValidator.DeepEqualsViaJson(_order, roundTripped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\AyCode.Core\AyCode.Core.csproj" />
|
<ProjectReference Include="..\AyCode.Core\AyCode.Core.csproj" />
|
||||||
<ProjectReference Include="..\AyCode.Core.Tests\AyCode.Core.Tests.csproj" />
|
<ProjectReference Include="..\AyCode.Core.Tests\AyCode.Core.Tests.csproj" />
|
||||||
|
<ProjectReference Include="..\AyCode.Benchmark\AyCode.Benchmark.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
@ -16,6 +17,7 @@
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<StartupObject>AyCode.Core.Serializers.Console.Program</StartupObject>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- AOT-mode is publish-time only.
|
<!-- AOT-mode is publish-time only.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
using AyCode.Core.Serializers.Binaries;
|
using AyCode.Core.Benchmarks.Reporting;
|
||||||
using AyCode.Core.Serializers.Console.Benchmarks;
|
using AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||||
|
using AyCode.Core.Serializers.Binaries;
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using MemoryPack;
|
using MemoryPack;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Console;
|
namespace AyCode.Core.Serializers.Console;
|
||||||
|
|
||||||
|
|
@ -403,7 +403,7 @@ internal static class BenchmarkLoop
|
||||||
//MakeAcBinary(testData, fastWireOptions, "FastMode (FastWire)"),
|
//MakeAcBinary(testData, fastWireOptions, "FastMode (FastWire)"),
|
||||||
// MemPack canonically on _All_True (no AcBinary opt-in/opt-out axis applies; the MemoryPackable
|
// 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).
|
// contract serialises identical bytes either way, but _All_True is the established baseline).
|
||||||
new MemoryPackBenchmark<TestOrder_All_False>(orderFalse, "Default"),
|
new MemoryPackBenchmark<TestOrder_All_False>(orderFalse, Configuration.SelectedWireMode, "Default"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -541,9 +541,9 @@ internal static class BenchmarkLoop
|
||||||
// MemoryPack — three I/O modes for apples-to-apples comparison
|
// MemoryPack — three I/O modes for apples-to-apples comparison
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// MemPack canonically on _All_True (see FastestByte-mode comment above for rationale).
|
// MemPack canonically on _All_True (see FastestByte-mode comment above for rationale).
|
||||||
new MemoryPackBenchmark<TestOrder_All_False>(orderFalse, "Default"),
|
new MemoryPackBenchmark<TestOrder_All_False>(orderFalse, Configuration.SelectedWireMode, "Default"),
|
||||||
new MemoryPackBufferWriterBenchmark<TestOrder_All_False>(orderFalse, "Default"),
|
new MemoryPackBufferWriterBenchmark<TestOrder_All_False>(orderFalse, Configuration.SelectedWireMode, "Default"),
|
||||||
new MemoryPackFreshBufferWriterBenchmark<TestOrder_All_False>(orderFalse, "Default"),
|
new MemoryPackFreshBufferWriterBenchmark<TestOrder_All_False>(orderFalse, Configuration.SelectedWireMode, "Default"),
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// MessagePack — for legacy comparison
|
// MessagePack — for legacy comparison
|
||||||
|
|
@ -795,44 +795,6 @@ internal static class BenchmarkLoop
|
||||||
_progressLastLineLen = 0;
|
_progressLastLineLen = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#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
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Round-trip equality check: serialize both via System.Text.Json (canonical form) and compare strings.
|
|
||||||
/// Slower than property-by-property compare, but universal — works for any object graph without custom comparer.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// AOT publish skip: <c>System.Text.Json</c>'s reflection path uses runtime closed-generic instantiation
|
|
||||||
/// (<c>JsonPropertyInfo<TestStatus></c> et al.) that the trimmer drops, causing
|
|
||||||
/// <c>NotSupportedException: missing native code or metadata</c>. The validation is JIT-only — the actual
|
|
||||||
/// benchmark Serialize/Deserialize loops don't touch this path. Under AOT we return <c>true</c> so all
|
|
||||||
/// <c>VerifyRoundTrip()</c> calls pass without running the cross-format validation.
|
|
||||||
/// </remarks>
|
|
||||||
internal 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
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates MemoryPack setup at startup. Aborts the benchmark if TestOrder_All_True is not [MemoryPackable].
|
/// Validates MemoryPack setup at startup. Aborts the benchmark if TestOrder_All_True is not [MemoryPackable].
|
||||||
/// Without this attribute, MemoryPack falls back to runtime resolver (slower) — comparison would be INVALID.
|
/// Without this attribute, MemoryPack falls back to runtime resolver (slower) — comparison would be INVALID.
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
using AyCode.Core.Serializers.Binaries;
|
|
||||||
using MemoryPack;
|
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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) and
|
|
||||||
/// the MemoryPack <c>WireMode</c>-aligned options selection (so AcBinary FastWire ↔ MemoryPack
|
|
||||||
/// UTF-16 comparisons stay apples-to-apples).
|
|
||||||
/// </summary>
|
|
||||||
internal static class BenchmarkOptions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 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. <c>Interning=All(opt) | False (attr)</c>) so attribute-suppressed features cannot
|
|
||||||
/// silently mislead. Pass any benchmark-specific extras (e.g. <c>", BufferSize=4096B"</c>)
|
|
||||||
/// in <paramref name="extra"/> — they are appended after the common fields.
|
|
||||||
/// </summary>
|
|
||||||
internal 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) | {Configuration.AttrFlags.refHandling} (attr), " +
|
|
||||||
$"Interning={options.UseStringInterning}(opt) | {Configuration.AttrFlags.internString} (attr), " +
|
|
||||||
$"Metadata={options.UseMetadata}(opt) | {Configuration.AttrFlags.metadata} (attr), " +
|
|
||||||
$"PropertyFilter={propFilterOpt}(opt) | {Configuration.AttrFlags.propertyFilter} (attr), " +
|
|
||||||
$"SGen={options.UseGeneratedCode}, " +
|
|
||||||
$"Compression={options.UseCompression}{extra}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns MemoryPack serializer options aligned with <see cref="Configuration.SelectedWireMode"/> for a fair
|
|
||||||
/// apples-to-apples wire-format comparison:
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item><see cref="WireMode.Compact"/> → <see cref="MemoryPackSerializerOptions.Default"/> (UTF-8) — both
|
|
||||||
/// engines encode UTF-8, comparison is purely about header / tier / dispatch overhead.</item>
|
|
||||||
/// <item><see cref="WireMode.Fast"/> → <see cref="MemoryPackSerializerOptions.Utf16"/> (UTF-16 raw memcpy) —
|
|
||||||
/// both engines write UTF-16 raw bytes, so wire-size and CPU comparison reflect the same string-encoding family.</item>
|
|
||||||
/// </list>
|
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
|
||||||
internal static MemoryPackSerializerOptions GetMemPack() =>
|
|
||||||
Configuration.SelectedWireMode == WireMode.Fast
|
|
||||||
? MemoryPackSerializerOptions.Utf16
|
|
||||||
: MemoryPackSerializerOptions.Default;
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
using AyCode.Core.Serializers.Attributes;
|
|
||||||
using AyCode.Core.Serializers.Binaries;
|
using AyCode.Core.Serializers.Binaries;
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using System.Reflection;
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
|
|
@ -76,37 +74,6 @@ internal static class Configuration
|
||||||
|
|
||||||
internal static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
|
internal static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Aggregated <see cref="AcBinarySerializableAttribute"/> feature flags across every type tagged with
|
|
||||||
/// the attribute in the loaded assemblies. Cached on first access (single reflection scan at startup).
|
|
||||||
/// Used by the benchmark's per-row Options-column formatter so the 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 → <c>true</c>; if any tagged type
|
|
||||||
/// disables it → <c>false</c> (a single disabling type suppresses the feature on the type-graph).
|
|
||||||
/// </summary>
|
|
||||||
internal 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<Type>(); } })
|
|
||||||
.Select(t => t.GetCustomAttribute<AcBinarySerializableAttribute>())
|
|
||||||
.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));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a human-readable name for the currently-active <c>BenchmarkTestDataProvider.LongStringSuffix</c>
|
/// Returns a human-readable name for the currently-active <c>BenchmarkTestDataProvider.LongStringSuffix</c>
|
||||||
/// charset. Returns "Custom" when the suffix doesn't match any of the predefined
|
/// charset. Returns "Custom" when the suffix doesn't match any of the predefined
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
using AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||||
using AyCode.Core.Serializers.Binaries;
|
using AyCode.Core.Serializers.Binaries;
|
||||||
using AyCode.Core.Serializers.Console.Benchmarks;
|
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Console;
|
namespace AyCode.Core.Serializers.Console;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
|
using AyCode.Core.Benchmarks.Reporting;
|
||||||
|
using AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||||
using AyCode.Core.Serializers.Binaries;
|
using AyCode.Core.Serializers.Binaries;
|
||||||
using AyCode.Core.Serializers.Console.Benchmarks;
|
|
||||||
using AyCode.Core.Tests.TestModels;
|
using AyCode.Core.Tests.TestModels;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
using AyCode.Core.Serializers.Console.Benchmarks;
|
using AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Console;
|
namespace AyCode.Core.Serializers.Console;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ public partial class AcBinarySourceGenerator
|
||||||
foreach (var p in ci.Properties)
|
foreach (var p in ci.Properties)
|
||||||
{
|
{
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
EmitReadProp(sb, p, " ", ci.EnableMetadata, ci.EnableInternString);
|
EmitReadProp(sb, p, " ", ci.EnableMetadata, ci.EnableInternString, ci.EnableRefHandling);
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.AppendLine(" }");
|
sb.AppendLine(" }");
|
||||||
|
|
@ -85,7 +85,7 @@ public partial class AcBinarySourceGenerator
|
||||||
/// Markered types: read type code byte, then dispatch.
|
/// Markered types: read type code byte, then dispatch.
|
||||||
/// Mirrors the serializer's EmitProp symmetry.
|
/// Mirrors the serializer's EmitProp symmetry.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void EmitReadProp(StringBuilder sb, PropInfo p, string i, bool enableMetadata, bool enableInternString)
|
private static void EmitReadProp(StringBuilder sb, PropInfo p, string i, bool enableMetadata, bool enableInternString, bool enableRefHandling)
|
||||||
{
|
{
|
||||||
var a = $"obj.{p.Name}";
|
var a = $"obj.{p.Name}";
|
||||||
|
|
||||||
|
|
@ -145,15 +145,15 @@ public partial class AcBinarySourceGenerator
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PropertyTypeKind.Complex:
|
case PropertyTypeKind.Complex:
|
||||||
EmitReadComplex(sb, p, a, tc, i + " ");
|
EmitReadComplex(sb, p, a, tc, i + " ", enableRefHandling);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PropertyTypeKind.Collection:
|
case PropertyTypeKind.Collection:
|
||||||
EmitReadCollection(sb, p, a, tc, i + " ", enableInternString);
|
EmitReadCollection(sb, p, a, tc, i + " ", enableInternString, enableRefHandling);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PropertyTypeKind.Dictionary:
|
case PropertyTypeKind.Dictionary:
|
||||||
EmitReadDictionary(sb, p, a, tc, i + " ", enableInternString);
|
EmitReadDictionary(sb, p, a, tc, i + " ", enableInternString, enableRefHandling);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
@ -267,7 +267,7 @@ public partial class AcBinarySourceGenerator
|
||||||
/// Non-nullable + no ref → ZERO branches (tc consumed but ignored).
|
/// Non-nullable + no ref → ZERO branches (tc consumed but ignored).
|
||||||
/// No SGen → runtime fallback via ReadValueGenerated.
|
/// No SGen → runtime fallback via ReadValueGenerated.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void EmitReadComplex(StringBuilder sb, PropInfo p, string a, string tc, string i)
|
private static void EmitReadComplex(StringBuilder sb, PropInfo p, string a, string tc, string i, bool enableRefHandling)
|
||||||
{
|
{
|
||||||
if (!p.HasGeneratedWriter)
|
if (!p.HasGeneratedWriter)
|
||||||
{
|
{
|
||||||
|
|
@ -292,7 +292,11 @@ public partial class AcBinarySourceGenerator
|
||||||
var reader = p.WriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader");
|
var reader = p.WriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader");
|
||||||
var cast = $"({p.TypeNameForTypeof})";
|
var cast = $"({p.TypeNameForTypeof})";
|
||||||
|
|
||||||
if (!p.ChildNeedsRefScan)
|
// Ref-aware switch ONLY when both (a) the parent type opts into ref handling via EnableRefHandlingFeature
|
||||||
|
// (otherwise no Complex property of this type's reader will ever see an ObjectRef* marker — writer never
|
||||||
|
// emits them on this type) AND (b) the child type subtree may emit ref markers (ChildNeedsRefScan).
|
||||||
|
// Either flag false → ZERO-branch path (Object / FixObj only).
|
||||||
|
if (!enableRefHandling || !p.ChildNeedsRefScan)
|
||||||
{
|
{
|
||||||
// Compile-time proven: child never tracked → only Object (+ Null for nullable) in stream
|
// Compile-time proven: child never tracked → only Object (+ Null for nullable) in stream
|
||||||
// Inline: parent creates instance, calls ReadProperties directly (mirrors EmitDirectObjectWrite)
|
// Inline: parent creates instance, calls ReadProperties directly (mirrors EmitDirectObjectWrite)
|
||||||
|
|
@ -384,12 +388,12 @@ public partial class AcBinarySourceGenerator
|
||||||
/// Known collection kind + inlineable element → inline Array loop with direct element reads.
|
/// Known collection kind + inlineable element → inline Array loop with direct element reads.
|
||||||
/// Else → runtime fallback via ReadValueGenerated.
|
/// Else → runtime fallback via ReadValueGenerated.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void EmitReadCollection(StringBuilder sb, PropInfo p, string a, string tc, string i, bool enableInternString)
|
private static void EmitReadCollection(StringBuilder sb, PropInfo p, string a, string tc, string i, bool enableInternString, bool enableRefHandling)
|
||||||
{
|
{
|
||||||
// Check if we can inline: known collection shape + inlineable element type
|
// Check if we can inline: known collection shape + inlineable element type
|
||||||
if (p.CollectionKind != null && CanInlineCollectionRead(p))
|
if (p.CollectionKind != null && CanInlineCollectionRead(p))
|
||||||
{
|
{
|
||||||
EmitReadCollectionInline(sb, p, a, tc, i, enableInternString);
|
EmitReadCollectionInline(sb, p, a, tc, i, enableInternString, enableRefHandling);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -415,7 +419,7 @@ public partial class AcBinarySourceGenerator
|
||||||
/// Wire format: [Dictionary][VarUInt count][key₁ value₁ key₂ value₂ ...].
|
/// Wire format: [Dictionary][VarUInt count][key₁ value₁ key₂ value₂ ...].
|
||||||
/// Keys and values are read inline when their types are known (primitive/string/Complex+SGen).
|
/// Keys and values are read inline when their types are known (primitive/string/Complex+SGen).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void EmitReadDictionary(StringBuilder sb, PropInfo p, string a, string tc, string i, bool enableInternString)
|
private static void EmitReadDictionary(StringBuilder sb, PropInfo p, string a, string tc, string i, bool enableInternString, bool enableRefHandling)
|
||||||
{
|
{
|
||||||
var s = p.Name;
|
var s = p.Name;
|
||||||
var keyType = p.DictKeyTypeName ?? "object";
|
var keyType = p.DictKeyTypeName ?? "object";
|
||||||
|
|
@ -462,6 +466,12 @@ public partial class AcBinarySourceGenerator
|
||||||
sb.AppendLine($"{i} {valReader}.Instance.ReadProperties(rv_{s}, context);");
|
sb.AppendLine($"{i} {valReader}.Instance.ReadProperties(rv_{s}, context);");
|
||||||
sb.AppendLine($"{i} dv_{s} = rv_{s};");
|
sb.AppendLine($"{i} dv_{s} = rv_{s};");
|
||||||
sb.AppendLine($"{i} }}");
|
sb.AppendLine($"{i} }}");
|
||||||
|
// ObjectRefFirst / ObjectRef cases — only emit when both (a) the parent type opts into
|
||||||
|
// ref handling and (b) the dict-value subtree may emit ref markers. Either flag false →
|
||||||
|
// skip these branches (writer never emits them; reader handles unknown markers via the
|
||||||
|
// fallback ReadValueGenerated path below). ACCORE-BIN-T-K9M3 Phase C step 2.
|
||||||
|
if (enableRefHandling && p.DictValueNeedsRefScan)
|
||||||
|
{
|
||||||
sb.AppendLine($"{i} else if ({vtc} == BinaryTypeCode.ObjectRefFirst)");
|
sb.AppendLine($"{i} else if ({vtc} == BinaryTypeCode.ObjectRefFirst)");
|
||||||
sb.AppendLine($"{i} {{");
|
sb.AppendLine($"{i} {{");
|
||||||
sb.AppendLine($"{i} var rci_{s} = (int)context.ReadVarUInt();");
|
sb.AppendLine($"{i} var rci_{s} = (int)context.ReadVarUInt();");
|
||||||
|
|
@ -472,6 +482,7 @@ public partial class AcBinarySourceGenerator
|
||||||
sb.AppendLine($"{i} }}");
|
sb.AppendLine($"{i} }}");
|
||||||
sb.AppendLine($"{i} else if ({vtc} == BinaryTypeCode.ObjectRef)");
|
sb.AppendLine($"{i} else if ({vtc} == BinaryTypeCode.ObjectRef)");
|
||||||
sb.AppendLine($"{i} dv_{s} = ({valType})context.GetInternedObject((int)context.ReadVarUInt())!;");
|
sb.AppendLine($"{i} dv_{s} = ({valType})context.GetInternedObject((int)context.ReadVarUInt())!;");
|
||||||
|
}
|
||||||
sb.AppendLine($"{i} else if ({vtc} != BinaryTypeCode.Null)");
|
sb.AppendLine($"{i} else if ({vtc} != BinaryTypeCode.Null)");
|
||||||
sb.AppendLine($"{i} {{");
|
sb.AppendLine($"{i} {{");
|
||||||
sb.AppendLine($"{i} context._position--;");
|
sb.AppendLine($"{i} context._position--;");
|
||||||
|
|
@ -595,7 +606,7 @@ public partial class AcBinarySourceGenerator
|
||||||
/// Reads count + loops with direct element reads (Complex with SGen, or primitive/string/enum).
|
/// Reads count + loops with direct element reads (Complex with SGen, or primitive/string/enum).
|
||||||
/// Eliminates per-element: ReadValue dispatch, ReadObjectCore dict lookup, Activator.CreateInstance.
|
/// Eliminates per-element: ReadValue dispatch, ReadObjectCore dict lookup, Activator.CreateInstance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void EmitReadCollectionInline(StringBuilder sb, PropInfo p, string a, string tc, string i, bool enableInternString)
|
private static void EmitReadCollectionInline(StringBuilder sb, PropInfo p, string a, string tc, string i, bool enableInternString, bool enableRefHandling)
|
||||||
{
|
{
|
||||||
var isComplexElement = p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter;
|
var isComplexElement = p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter;
|
||||||
var elemType = p.ElementFullTypeName!;
|
var elemType = p.ElementFullTypeName!;
|
||||||
|
|
@ -622,7 +633,7 @@ public partial class AcBinarySourceGenerator
|
||||||
sb.AppendLine($"{i} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)");
|
sb.AppendLine($"{i} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)");
|
||||||
sb.AppendLine($"{i} {{");
|
sb.AppendLine($"{i} {{");
|
||||||
if (isComplexElement)
|
if (isComplexElement)
|
||||||
EmitReadCollectionElement(sb, p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"), elemType, $"({elemType})", $"ri_{s}", s, i + " ", isArray: true, p.ElementNeedsRefScan, enableInternString);
|
EmitReadCollectionElement(sb, p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"), elemType, $"({elemType})", $"ri_{s}", s, i + " ", isArray: true, p.ElementNeedsRefScan, enableInternString, enableRefHandling);
|
||||||
else
|
else
|
||||||
EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: true, null, enableInternString);
|
EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: true, null, enableInternString);
|
||||||
sb.AppendLine($"{i} }}");
|
sb.AppendLine($"{i} }}");
|
||||||
|
|
@ -637,7 +648,7 @@ public partial class AcBinarySourceGenerator
|
||||||
sb.AppendLine($"{i} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)");
|
sb.AppendLine($"{i} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)");
|
||||||
sb.AppendLine($"{i} {{");
|
sb.AppendLine($"{i} {{");
|
||||||
if (isComplexElement)
|
if (isComplexElement)
|
||||||
EmitReadCollectionElement(sb, p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"), elemType, $"({elemType})", $"ri_{s}", s, i + " ", isArray: false, p.ElementNeedsRefScan, enableInternString, p.CollectionAddMethod);
|
EmitReadCollectionElement(sb, p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"), elemType, $"({elemType})", $"ri_{s}", s, i + " ", isArray: false, p.ElementNeedsRefScan, enableInternString, enableRefHandling, p.CollectionAddMethod);
|
||||||
else
|
else
|
||||||
EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: false, p.CollectionAddMethod, enableInternString);
|
EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: false, p.CollectionAddMethod, enableInternString);
|
||||||
sb.AppendLine($"{i} }}");
|
sb.AppendLine($"{i} }}");
|
||||||
|
|
@ -648,7 +659,7 @@ public partial class AcBinarySourceGenerator
|
||||||
sb.AppendLine($"{i} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)");
|
sb.AppendLine($"{i} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)");
|
||||||
sb.AppendLine($"{i} {{");
|
sb.AppendLine($"{i} {{");
|
||||||
if (isComplexElement)
|
if (isComplexElement)
|
||||||
EmitReadCollectionElement(sb, p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"), elemType, $"({elemType})", $"ri_{s}", s, i + " ", isArray: false, p.ElementNeedsRefScan, enableInternString);
|
EmitReadCollectionElement(sb, p.ElementWriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader"), elemType, $"({elemType})", $"ri_{s}", s, i + " ", isArray: false, p.ElementNeedsRefScan, enableInternString, enableRefHandling);
|
||||||
else
|
else
|
||||||
EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: false, null, enableInternString);
|
EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: false, null, enableInternString);
|
||||||
sb.AppendLine($"{i} }}");
|
sb.AppendLine($"{i} }}");
|
||||||
|
|
@ -663,7 +674,7 @@ public partial class AcBinarySourceGenerator
|
||||||
/// SGen reader = non-metadata mode → no ObjectWithMetadata fallback.
|
/// SGen reader = non-metadata mode → no ObjectWithMetadata fallback.
|
||||||
/// !needsRefScan → only Object/Null possible → 1 branch per element.
|
/// !needsRefScan → only Object/Null possible → 1 branch per element.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void EmitReadCollectionElement(StringBuilder sb, string reader, string elemTypeName, string elemCast, string indexVar, string propSuffix, string i, bool isArray, bool needsRefScan, bool enableInternString, string? addMethod = null)
|
private static void EmitReadCollectionElement(StringBuilder sb, string reader, string elemTypeName, string elemCast, string indexVar, string propSuffix, string i, bool isArray, bool needsRefScan, bool enableInternString, bool enableRefHandling, string? addMethod = null)
|
||||||
{
|
{
|
||||||
var etc = $"etc_{propSuffix}";
|
var etc = $"etc_{propSuffix}";
|
||||||
sb.AppendLine($"{i}var {etc} = context.ReadByte();");
|
sb.AppendLine($"{i}var {etc} = context.ReadByte();");
|
||||||
|
|
@ -672,7 +683,9 @@ public partial class AcBinarySourceGenerator
|
||||||
var assignNull = isArray ? $"col_{propSuffix}[{indexVar}] = null!;" : $"col_{propSuffix}.{addCall}(null!);";
|
var assignNull = isArray ? $"col_{propSuffix}[{indexVar}] = null!;" : $"col_{propSuffix}.{addCall}(null!);";
|
||||||
var assignExpr = isArray ? $"col_{propSuffix}[{indexVar}] = re_{propSuffix};" : $"col_{propSuffix}.{addCall}(re_{propSuffix});";
|
var assignExpr = isArray ? $"col_{propSuffix}[{indexVar}] = re_{propSuffix};" : $"col_{propSuffix}.{addCall}(re_{propSuffix});";
|
||||||
|
|
||||||
if (!needsRefScan)
|
// Ref-aware switch ONLY when both the parent type opts in (EnableRefHandlingFeature) and the
|
||||||
|
// element subtree may emit ref markers (needsRefScan). Either flag false → ZERO-branch path.
|
||||||
|
if (!enableRefHandling || !needsRefScan)
|
||||||
{
|
{
|
||||||
// No ref tracking → only Object, FixObj or Null in stream — inline ReadProperties
|
// No ref tracking → only Object, FixObj or Null in stream — inline ReadProperties
|
||||||
// FixObj slot: populate slot cache to keep _nextRuntimeSlot in sync.
|
// FixObj slot: populate slot cache to keep _nextRuntimeSlot in sync.
|
||||||
|
|
|
||||||
|
|
@ -413,6 +413,7 @@ Global
|
||||||
{A20861A9-411E-6150-BF5C-69E8196E5D22}.Product|x86.ActiveCfg = Product|Any CPU
|
{A20861A9-411E-6150-BF5C-69E8196E5D22}.Product|x86.ActiveCfg = Product|Any CPU
|
||||||
{A20861A9-411E-6150-BF5C-69E8196E5D22}.Product|x86.Build.0 = Product|Any CPU
|
{A20861A9-411E-6150-BF5C-69E8196E5D22}.Product|x86.Build.0 = Product|Any CPU
|
||||||
{A20861A9-411E-6150-BF5C-69E8196E5D22}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{A20861A9-411E-6150-BF5C-69E8196E5D22}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A20861A9-411E-6150-BF5C-69E8196E5D22}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{A20861A9-411E-6150-BF5C-69E8196E5D22}.Release|x64.ActiveCfg = Release|Any CPU
|
{A20861A9-411E-6150-BF5C-69E8196E5D22}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{A20861A9-411E-6150-BF5C-69E8196E5D22}.Release|x64.Build.0 = Release|Any CPU
|
{A20861A9-411E-6150-BF5C-69E8196E5D22}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{A20861A9-411E-6150-BF5C-69E8196E5D22}.Release|x86.ActiveCfg = Release|Any CPU
|
{A20861A9-411E-6150-BF5C-69E8196E5D22}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,51 @@ namespace AyCode.Core.Serializers.Attributes;
|
||||||
public sealed class AcBinarySerializableAttribute : Attribute
|
public sealed class AcBinarySerializableAttribute : Attribute
|
||||||
{
|
{
|
||||||
public bool EnableMetadataFeature { get; }
|
public bool EnableMetadataFeature { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When <c>true</c> (default): the SGen-emitted code recognizes the type's tracking-Id property
|
||||||
|
/// and emits Id-keyed reference deduplication support, active when <c>ReferenceHandling</c> is
|
||||||
|
/// <c>OnlyId</c> or <c>All</c>.
|
||||||
|
/// <para>When <c>false</c>: Id-tracking emit is skipped entirely. <b>Significantly reduces
|
||||||
|
/// scan-pass cost</b> — the per-instance <c>wrapper.TryTrack*</c> call + <c>IdentityMap</c>
|
||||||
|
/// lookup is one of the dominant scan-phase costs. Combined with <c>EnableRefHandlingFeature
|
||||||
|
/// = false</c>, the scan body for this type collapses to near-no-op. Reader-side
|
||||||
|
/// <c>ObjectRefFirst</c> / <c>ObjectRef</c> case-emit also strips when the child subtree's
|
||||||
|
/// tracking is provably absent. The runtime <c>ReferenceHandling</c> option is silently
|
||||||
|
/// ignored for instances of this type. Use only when this type is never shared by Id (e.g.
|
||||||
|
/// immutable value-DTO, single-use message payload, append-only log entry).</para>
|
||||||
|
/// </summary>
|
||||||
public bool EnableIdTrackingFeature { get; }
|
public bool EnableIdTrackingFeature { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When <c>true</c> (default): the SGen-emitted code emits non-IId reference tracking
|
||||||
|
/// (<c>wrapper.TryTrackInt32(GetHashCode(...))</c> in the scan pass when <c>ReferenceHandling
|
||||||
|
/// = All</c>) AND the reader-side <c>ObjectRef</c> / <c>ObjectRefFirst</c> / <c>ObjectWithMetadataRefFirst</c>
|
||||||
|
/// case-emit on every Complex / Collection-element / Dictionary-value property of this type.
|
||||||
|
/// <para>When <c>false</c>: both emit blocks are omitted. <b>Significantly reduces scan-pass
|
||||||
|
/// cost</b> — the per-instance hash-track lookup is eliminated; combined with
|
||||||
|
/// <c>EnableIdTrackingFeature = false</c> the scan pass for this type degenerates to a primitive-property
|
||||||
|
/// iteration only. Reader-side switch-dispatch shrinks by 2-3 cases per Complex/Collection/Dict
|
||||||
|
/// property (smaller jump table, better branch predictor, smaller IL). The runtime
|
||||||
|
/// <c>ReferenceHandling</c> option is silently ignored for instances of this type. Use only when
|
||||||
|
/// the type is never reference-shared across the serialized graph.</para>
|
||||||
|
/// </summary>
|
||||||
public bool EnableRefHandlingFeature { get; }
|
public bool EnableRefHandlingFeature { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When <c>true</c> (default): the SGen-emitted code emits string-interning support for
|
||||||
|
/// intern-eligible properties — <c>context.ScanInternString(str)</c> in the scan pass and
|
||||||
|
/// <c>StringInterned</c> / <c>StringInternFirstSmall</c> / <c>StringInternFirstMedium</c>
|
||||||
|
/// case-emit in the reader.
|
||||||
|
/// <para>When <c>false</c>: both emit blocks are omitted. <b>Significantly reduces scan-pass
|
||||||
|
/// cost</b> — string-property iteration with <c>IdentityMap</c> lookup is eliminated entirely
|
||||||
|
/// (often the heaviest scan-phase work on string-rich DTOs). The writer's per-property
|
||||||
|
/// <c>StringInternEligible</c> flag is always <c>false</c> for this type, so plan-entry
|
||||||
|
/// consumption never fires. Reader switch dispatch shrinks by 3 cases per string property
|
||||||
|
/// (smaller jump table, better branch predictor). The runtime <c>UseStringInterning</c> option
|
||||||
|
/// is silently ignored for instances of this type. Use only when this type's strings have low
|
||||||
|
/// intern-yield (unique per instance — Ids, GUIDs, free-text, never-repeated content).</para>
|
||||||
|
/// </summary>
|
||||||
public bool EnableInternStringFeature { get; }
|
public bool EnableInternStringFeature { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue