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 == 68 { print; print \"\"; print \" // Writer-side emit pass \\(GenWriter + GenScanProperties + EmitProp + EmitScan* + EmitDirect*Write +\"; print \" // EmitSkip + EmitVal + EmitMarkerless + helpers\\) moved to AcBinarySourceGenerator.GenWriter.cs.\"; next } { print }' tmp.cs)",
|
||||
"Bash(awk 'NR < 73 || NR > 930 { print }' AcBinarySourceGenerator.cs)",
|
||||
"Bash(awk 'NR == 72 { print; print \" // Reader-side emit pass \\(GenReader + EmitReadProp + EmitRead* helpers\\) moved to\"; print \" // AcBinarySourceGenerator.GenReader.cs.\"; next } { print }' tmp.cs)"
|
||||
"Bash(awk 'NR == 72 { print; print \" // Reader-side emit pass \\(GenReader + EmitReadProp + EmitRead* helpers\\) moved to\"; print \" // AcBinarySourceGenerator.GenReader.cs.\"; next } { print }' tmp.cs)",
|
||||
"Bash(rm -f \"AyCode.Core.Serializers.Console/Benchmarks/\"*.cs && rmdir \"AyCode.Core.Serializers.Console/Benchmarks\" && rm -f \"AyCode.Core.Serializers.Console/BenchmarkResult.cs\" && echo \"Deleted Console-side moved files.\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
|
||||
<PackageReference Include="MemoryPack" Version="1.21.4" />
|
||||
<PackageReference Include="MessagePack" Version="3.1.4" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.DiagnosticsHub.BenchmarkDotNetDiagnosers" Version="18.6.37110.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>
|
||||
/// Per-cell benchmark result row. Populated by the benchmark execution loop in
|
||||
/// <c>BenchmarkLoop.RunBenchmarksForTestData</c>; consumed by the output formatters in
|
||||
/// <c>Output</c> (console table + .log + .LLM file writers). Pure DTO — no behaviour.
|
||||
/// Per-cell benchmark result row. Populated by the benchmark execution loop (Console-side
|
||||
/// <c>BenchmarkLoop.RunBenchmarksForTestData</c> / BDN-side <c>BdnSummaryAdapter</c>); consumed by the
|
||||
/// output formatters in <c>BenchmarkReportWriter</c> (console table + .log + .LLM file writers).
|
||||
/// Pure DTO — no behaviour.
|
||||
/// </summary>
|
||||
internal sealed class BenchmarkResult
|
||||
public sealed class BenchmarkResult
|
||||
{
|
||||
public string TestDataName { get; set; } = "";
|
||||
public BenchmarkEngine Engine { get; set; }
|
||||
|
|
@ -18,7 +19,7 @@ internal sealed class BenchmarkResult
|
|||
/// <summary>
|
||||
/// 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>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
|
||||
/// without inflating the per-row tables with an extra column.
|
||||
/// </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 System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
||||
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
internal sealed class AcBinaryBenchmark<T> : ISerializerBenchmark where T : class
|
||||
public sealed class AcBinaryBenchmark<T> : ISerializerBenchmark where T : class
|
||||
{
|
||||
private readonly T _order;
|
||||
private readonly AcBinarySerializerOptions _options;
|
||||
|
|
@ -42,6 +42,6 @@ internal sealed class AcBinaryBenchmark<T> : ISerializerBenchmark where T : clas
|
|||
{
|
||||
var bytes = AcBinarySerializer.Serialize(_order, _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 System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
||||
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </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 AcBinarySerializerOptions _options;
|
||||
|
|
@ -64,6 +64,6 @@ internal sealed class AcBinaryBufferWriterBenchmark<T> : ISerializerBenchmark wh
|
|||
AcBinarySerializer.Serialize(_order, _bufferWriter, _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 System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
||||
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks AcBinary via the IBufferWriter overload, allocating a FRESH ArrayBufferWriter on EVERY call.
|
||||
|
|
@ -12,7 +12,7 @@ namespace AyCode.Core.Serializers.Console.Benchmarks;
|
|||
/// otherwise AcBinary would request 64KB upfront via GetSpan(), forcing the fresh ABW to allocate 64KB
|
||||
/// regardless of payload size (heavy over-allocation for small payloads).
|
||||
/// </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 AcBinarySerializerOptions _options;
|
||||
|
|
@ -59,6 +59,6 @@ internal sealed class AcBinaryFreshBufferWriterBenchmark<T> : ISerializerBenchma
|
|||
var abw = new ArrayBufferWriter<byte>();
|
||||
AcBinarySerializer.Serialize(_order, abw, _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.TestModels;
|
||||
using System.IO.Pipelines;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
||||
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||
|
||||
/// <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
|
||||
/// <c>Writer</c>/<c>Reader</c> pair is a managed-only zero-copy slab handoff — no syscalls, no kernel
|
||||
/// buffer copy, no IRP queueing.
|
||||
|
|
@ -15,16 +15,16 @@ namespace AyCode.Core.Serializers.Console.Benchmarks;
|
|||
/// <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
|
||||
/// 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
|
||||
/// 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
|
||||
/// pipelines inside a single process (e.g. SignalR's Kestrel transport adapter, gRPC framework internals,
|
||||
/// custom message brokers). The numbers from this row reflect that scenario, NOT the kernel-pipe loopback
|
||||
/// of the NamedPipe benchmark.</para>
|
||||
/// </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 AcBinarySerializerOptions _options;
|
||||
|
|
@ -160,7 +160,7 @@ internal sealed class AcBinaryInMemoryPipeBenchmark<T> : ISerializerBenchmark, I
|
|||
{
|
||||
Serialize();
|
||||
var result = _lastResult as T;
|
||||
return result != null && BenchmarkLoop.DeepEqualsViaJson(_order, result);
|
||||
return result != null && RoundTripValidator.DeepEqualsViaJson(_order, result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
||||
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// <list type="bullet">
|
||||
/// <item><description><b>NamedPipe + Chunked</b> = <see cref="AcBinaryNamedPipeBenchmark"/></description></item>
|
||||
/// <item><description><b>NamedPipe + Raw</b> = <see cref="AcBinaryNamedPipeRawByteArrayBenchmark"/></description></item>
|
||||
/// <item><description><b>In-memory Pipe + Chunked</b> = <see cref="AcBinaryInMemoryPipeBenchmark"/></description></item>
|
||||
/// <item><description><b>NamedPipe + Chunked</b> = <see cref="AcBinaryNamedPipeBenchmark{T}"/></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{T}"/></description></item>
|
||||
/// <item><description><b>In-memory + Raw</b> = THIS row — apples-to-apples baseline for the in-memory chunked row</description></item>
|
||||
/// </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.
|
||||
/// 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>
|
||||
/// </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 AcBinarySerializerOptions _options;
|
||||
|
|
@ -144,7 +144,7 @@ internal sealed class AcBinaryInMemoryRawByteArrayBenchmark<T> : ISerializerBenc
|
|||
{
|
||||
Serialize();
|
||||
var result = _lastResult as T;
|
||||
return result != null && BenchmarkLoop.DeepEqualsViaJson(_order, result);
|
||||
return result != null && RoundTripValidator.DeepEqualsViaJson(_order, result);
|
||||
}
|
||||
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.TestModels;
|
||||
using System.IO.Pipelines;
|
||||
using System.IO.Pipes;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
||||
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// adds further transport latency (TCP, WebSocket framing) on top. The benchmark gives a lower bound.</para>
|
||||
/// </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 AcBinarySerializerOptions _options;
|
||||
|
|
@ -205,7 +205,7 @@ internal sealed class AcBinaryNamedPipeBenchmark<T> : ISerializerBenchmark, IDis
|
|||
{
|
||||
Serialize();
|
||||
var result = _lastResult as T;
|
||||
return result != null && BenchmarkLoop.DeepEqualsViaJson(_order, result);
|
||||
return result != null && RoundTripValidator.DeepEqualsViaJson(_order, result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
using System.IO.Pipes;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
||||
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// 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>:
|
||||
/// <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>
|
||||
/// <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
|
||||
/// <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>
|
||||
/// </list>
|
||||
/// 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>).
|
||||
/// </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 AcBinarySerializerOptions _options;
|
||||
|
|
@ -185,7 +185,7 @@ internal sealed class AcBinaryNamedPipeRawByteArrayBenchmark<T> : ISerializerBen
|
|||
{
|
||||
Serialize();
|
||||
var result = _lastResult as T;
|
||||
return result != null && BenchmarkLoop.DeepEqualsViaJson(_order, result);
|
||||
return result != null && RoundTripValidator.DeepEqualsViaJson(_order, result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
||||
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// the human-readable form.
|
||||
/// </summary>
|
||||
internal enum BenchmarkEngine
|
||||
public enum BenchmarkEngine
|
||||
{
|
||||
AcBinary,
|
||||
MemoryPack,
|
||||
|
|
@ -21,7 +21,7 @@ internal enum BenchmarkEngine
|
|||
/// (they distinguish chunked-framed vs raw-byte[] semantics, but render identically in the IO column);
|
||||
/// the same applies to <see cref="InMemoryPipe"/> + <see cref="InMemoryRaw"/> (<c>"Pipe(in-mem)"</c>).
|
||||
/// </summary>
|
||||
internal enum BenchmarkIoMode
|
||||
public enum BenchmarkIoMode
|
||||
{
|
||||
ByteArray,
|
||||
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>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
internal enum BenchmarkDispatchMode
|
||||
public enum BenchmarkDispatchMode
|
||||
{
|
||||
SGen,
|
||||
Runtime,
|
||||
|
|
@ -50,7 +50,7 @@ internal enum BenchmarkDispatchMode
|
|||
}
|
||||
|
||||
/// <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
|
||||
/// <see cref="Enum.TryParse{T}(string, bool, out T)"/> with <c>ignoreCase: true</c>.
|
||||
/// <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>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
internal enum BenchmarkLayer
|
||||
public enum BenchmarkLayer
|
||||
{
|
||||
All,
|
||||
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
|
||||
/// <c>mode</c>/<c>opMode</c> parameter.
|
||||
/// </summary>
|
||||
internal enum BenchmarkOpMode
|
||||
public enum BenchmarkOpMode
|
||||
{
|
||||
All,
|
||||
Serialize,
|
||||
|
|
@ -86,7 +86,7 @@ internal enum BenchmarkOpMode
|
|||
}
|
||||
|
||||
/// <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>
|
||||
/// parameter.
|
||||
/// <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>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
internal enum SerializerSelectionMode
|
||||
public enum SerializerSelectionMode
|
||||
{
|
||||
Standard,
|
||||
FastestByte,
|
||||
|
|
@ -107,9 +107,9 @@ internal enum SerializerSelectionMode
|
|||
/// human-readable form used by the per-row console table, the <c>.log</c> file CSV/formatted output,
|
||||
/// and the <c>.LLM</c> markdown table. Centralised here so every output formatter renders identically.
|
||||
/// </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.MemoryPack => "MemoryPack",
|
||||
|
|
@ -120,7 +120,7 @@ internal static class BenchmarkEnumExtensions
|
|||
_ => throw new ArgumentOutOfRangeException(nameof(engine), engine, null),
|
||||
};
|
||||
|
||||
internal static string ToDisplay(this BenchmarkIoMode mode) => mode switch
|
||||
public static string ToDisplay(this BenchmarkIoMode mode) => mode switch
|
||||
{
|
||||
BenchmarkIoMode.ByteArray => "Byte[]",
|
||||
BenchmarkIoMode.BufWrReuse => "BufWr reuse",
|
||||
|
|
@ -133,7 +133,7 @@ internal static class BenchmarkEnumExtensions
|
|||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
||||
};
|
||||
|
||||
internal static string ToDisplay(this BenchmarkDispatchMode mode) => mode switch
|
||||
public static string ToDisplay(this BenchmarkDispatchMode mode) => mode switch
|
||||
{
|
||||
BenchmarkDispatchMode.SGen => "SGen",
|
||||
BenchmarkDispatchMode.Runtime => "Runtime",
|
||||
|
|
@ -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>
|
||||
/// 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"/>
|
||||
/// to true so the bench loop skips the Des-phase and routes timing into the RT columns.</para>
|
||||
/// </summary>
|
||||
internal interface ISerializerBenchmark
|
||||
public interface ISerializerBenchmark
|
||||
{
|
||||
/// <summary>Serializer engine — typed enum, see <see cref="BenchmarkEnumExtensions.ToDisplay(BenchmarkEngine)"/> for the human-readable form.</summary>
|
||||
BenchmarkEngine Engine { get; }
|
||||
|
|
@ -24,7 +24,7 @@ internal interface ISerializerBenchmark
|
|||
/// <summary>
|
||||
/// 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
|
||||
/// (<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.
|
||||
/// </summary>
|
||||
Type OrderType { get; }
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
using AyCode.Core.Tests.TestModels;
|
||||
using AyCode.Core.Serializers;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
using MemoryPack;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
||||
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// and FastWire ↔ UTF-16 are apples-to-apples on the string-encoding axis.
|
||||
/// </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 MemoryPackSerializerOptions _options;
|
||||
|
|
@ -25,11 +26,11 @@ internal sealed class MemoryPackBenchmark<T> : ISerializerBenchmark where T : cl
|
|||
public long SetupDeserializeAllocBytes => 0;
|
||||
public string? OptionsDescription => $"StringEncoding={_options.StringEncoding}";
|
||||
|
||||
public MemoryPackBenchmark(T order, string optionsPreset)
|
||||
public MemoryPackBenchmark(T order, WireMode wireMode, string optionsPreset)
|
||||
{
|
||||
_order = order;
|
||||
OptionsPreset = optionsPreset;
|
||||
_options = BenchmarkOptions.GetMemPack();
|
||||
_options = BenchmarkOptions.GetMemPack(wireMode);
|
||||
_serialized = MemoryPackSerializer.Serialize(order, _options);
|
||||
}
|
||||
|
||||
|
|
@ -43,6 +44,6 @@ internal sealed class MemoryPackBenchmark<T> : ISerializerBenchmark where T : cl
|
|||
{
|
||||
var bytes = MemoryPackSerializer.Serialize(_order, _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 System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
||||
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </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 MemoryPackSerializerOptions _options;
|
||||
|
|
@ -27,11 +28,11 @@ internal sealed class MemoryPackBufferWriterBenchmark<T> : ISerializerBenchmark
|
|||
public long SetupDeserializeAllocBytes => 0;
|
||||
public string? OptionsDescription => $"StringEncoding={_options.StringEncoding}";
|
||||
|
||||
public MemoryPackBufferWriterBenchmark(T order, string optionsPreset)
|
||||
public MemoryPackBufferWriterBenchmark(T order, WireMode wireMode, string optionsPreset)
|
||||
{
|
||||
_order = order;
|
||||
OptionsPreset = optionsPreset;
|
||||
_options = BenchmarkOptions.GetMemPack();
|
||||
_options = BenchmarkOptions.GetMemPack(wireMode);
|
||||
_serialized = MemoryPackSerializer.Serialize(order, _options);
|
||||
|
||||
// Serialize-side setup only — see AcBinaryBufferWriterBenchmark for the full rationale.
|
||||
|
|
@ -59,6 +60,6 @@ internal sealed class MemoryPackBufferWriterBenchmark<T> : ISerializerBenchmark
|
|||
_bufferWriter.ResetWrittenCount();
|
||||
MemoryPackSerializer.Serialize(_bufferWriter, _order, _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 System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
||||
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
internal sealed class MemoryPackFreshBufferWriterBenchmark<T> : ISerializerBenchmark where T : class
|
||||
public sealed class MemoryPackFreshBufferWriterBenchmark<T> : ISerializerBenchmark where T : class
|
||||
{
|
||||
private readonly T _order;
|
||||
private readonly MemoryPackSerializerOptions _options;
|
||||
|
|
@ -25,11 +26,11 @@ internal sealed class MemoryPackFreshBufferWriterBenchmark<T> : ISerializerBench
|
|||
public long SetupDeserializeAllocBytes => 0;
|
||||
public string? OptionsDescription => $"StringEncoding={_options.StringEncoding}";
|
||||
|
||||
public MemoryPackFreshBufferWriterBenchmark(T order, string optionsPreset)
|
||||
public MemoryPackFreshBufferWriterBenchmark(T order, WireMode wireMode, string optionsPreset)
|
||||
{
|
||||
_order = order;
|
||||
OptionsPreset = optionsPreset;
|
||||
_options = BenchmarkOptions.GetMemPack();
|
||||
_options = BenchmarkOptions.GetMemPack(wireMode);
|
||||
_serialized = MemoryPackSerializer.Serialize(order, _options);
|
||||
}
|
||||
|
||||
|
|
@ -50,6 +51,6 @@ internal sealed class MemoryPackFreshBufferWriterBenchmark<T> : ISerializerBench
|
|||
var abw = new ArrayBufferWriter<byte>();
|
||||
MemoryPackSerializer.Serialize(abw, _order, _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 MessagePack;
|
||||
using MessagePack.Resolvers;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
||||
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||
|
||||
/// <summary>
|
||||
/// MessagePack benchmark, Byte[] I/O mode. Excluded from NativeAOT build because v3's StandardResolver
|
||||
|
|
@ -12,7 +12,7 @@ namespace AyCode.Core.Serializers.Console.Benchmarks;
|
|||
/// which uses Activator.CreateInstance on formatter types the AOT trimmer drops →
|
||||
/// MissingMethodException at runtime. Available for regular JIT runs (<c>dotnet run</c>) only.
|
||||
/// </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 MessagePackSerializerOptions _options;
|
||||
|
|
@ -53,7 +53,7 @@ internal sealed class MessagePackBenchmark<T> : ISerializerBenchmark where T : c
|
|||
{
|
||||
var bytes = MessagePackSerializer.Serialize(_order, _options);
|
||||
var roundTripped = MessagePackSerializer.Deserialize<T>(bytes, _options);
|
||||
return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped);
|
||||
return RoundTripValidator.DeepEqualsViaJson(_order, roundTripped);
|
||||
}
|
||||
}
|
||||
#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.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AyCode.Core.Serializers.Console.Benchmarks;
|
||||
namespace AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <c>BenchmarkLoop.CreateSerializers</c>); ranks far behind binary serializers on µs/op but provides
|
||||
/// (no source-generator opt-in here). Typically NOT in the active suite (commented out in the
|
||||
/// caller-side <c>CreateSerializers</c>); ranks far behind binary serializers on µs/op but provides
|
||||
/// a familiar JSON baseline when needed.
|
||||
/// </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 JsonSerializerOptions _options;
|
||||
|
|
@ -37,7 +38,10 @@ internal sealed class SystemTextJsonBenchmark<T> : ISerializerBenchmark where T
|
|||
ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles
|
||||
};
|
||||
_serialized = JsonSerializer.Serialize(order, _options);
|
||||
_serializedUtf8 = Configuration.Utf8NoBom.GetBytes(_serialized);
|
||||
// Encoding.UTF8.GetBytes(string) does NOT prepend the BOM (the preamble is only emitted by
|
||||
// GetPreamble() / stream-level writes), so this produces identical bytes to the prior
|
||||
// `new UTF8Encoding(false).GetBytes(_serialized)` call. Size-reporting only.
|
||||
_serializedUtf8 = Encoding.UTF8.GetBytes(_serialized);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
|
|
@ -50,6 +54,6 @@ internal sealed class SystemTextJsonBenchmark<T> : ISerializerBenchmark where T
|
|||
{
|
||||
var json = JsonSerializer.Serialize(_order, _options);
|
||||
var roundTripped = JsonSerializer.Deserialize<T>(json, _options);
|
||||
return BenchmarkLoop.DeepEqualsViaJson(_order, roundTripped);
|
||||
return RoundTripValidator.DeepEqualsViaJson(_order, roundTripped);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
<ItemGroup>
|
||||
<ProjectReference Include="..\AyCode.Core\AyCode.Core.csproj" />
|
||||
<ProjectReference Include="..\AyCode.Core.Tests\AyCode.Core.Tests.csproj" />
|
||||
<ProjectReference Include="..\AyCode.Benchmark\AyCode.Benchmark.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -16,6 +17,7 @@
|
|||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<StartupObject>AyCode.Core.Serializers.Console.Program</StartupObject>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- AOT-mode is publish-time only.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Serializers.Console.Benchmarks;
|
||||
using AyCode.Core.Benchmarks.Reporting;
|
||||
using AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
using MemoryPack;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AyCode.Core.Serializers.Console;
|
||||
|
||||
|
|
@ -403,7 +403,7 @@ internal static class BenchmarkLoop
|
|||
//MakeAcBinary(testData, fastWireOptions, "FastMode (FastWire)"),
|
||||
// MemPack canonically on _All_True (no AcBinary opt-in/opt-out axis applies; the MemoryPackable
|
||||
// contract serialises identical bytes either way, but _All_True is the established baseline).
|
||||
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
|
||||
// ============================================================
|
||||
// MemPack canonically on _All_True (see FastestByte-mode comment above for rationale).
|
||||
new MemoryPackBenchmark<TestOrder_All_False>(orderFalse, "Default"),
|
||||
new MemoryPackBufferWriterBenchmark<TestOrder_All_False>(orderFalse, "Default"),
|
||||
new MemoryPackFreshBufferWriterBenchmark<TestOrder_All_False>(orderFalse, "Default"),
|
||||
new MemoryPackBenchmark<TestOrder_All_False>(orderFalse, Configuration.SelectedWireMode, "Default"),
|
||||
new MemoryPackBufferWriterBenchmark<TestOrder_All_False>(orderFalse, Configuration.SelectedWireMode, "Default"),
|
||||
new MemoryPackFreshBufferWriterBenchmark<TestOrder_All_False>(orderFalse, Configuration.SelectedWireMode, "Default"),
|
||||
|
||||
// ============================================================
|
||||
// MessagePack — for legacy comparison
|
||||
|
|
@ -795,44 +795,6 @@ internal static class BenchmarkLoop
|
|||
_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>
|
||||
/// 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.
|
||||
|
|
|
|||
|
|
@ -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.Tests.TestModels;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
|
|
@ -76,37 +74,6 @@ internal static class Configuration
|
|||
|
||||
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>
|
||||
/// 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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Serializers.Console.Benchmarks;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
|
||||
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.Console.Benchmarks;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ using System.Runtime.CompilerServices;
|
|||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using AyCode.Core.Serializers.Console.Benchmarks;
|
||||
using AyCode.Core.Benchmarks.Workloads.Scenarios;
|
||||
|
||||
namespace AyCode.Core.Serializers.Console;
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ public partial class AcBinarySourceGenerator
|
|||
foreach (var p in ci.Properties)
|
||||
{
|
||||
sb.AppendLine();
|
||||
EmitReadProp(sb, p, " ", ci.EnableMetadata, ci.EnableInternString);
|
||||
EmitReadProp(sb, p, " ", ci.EnableMetadata, ci.EnableInternString, ci.EnableRefHandling);
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
|
|
@ -85,7 +85,7 @@ public partial class AcBinarySourceGenerator
|
|||
/// Markered types: read type code byte, then dispatch.
|
||||
/// Mirrors the serializer's EmitProp symmetry.
|
||||
/// </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}";
|
||||
|
||||
|
|
@ -145,15 +145,15 @@ public partial class AcBinarySourceGenerator
|
|||
break;
|
||||
|
||||
case PropertyTypeKind.Complex:
|
||||
EmitReadComplex(sb, p, a, tc, i + " ");
|
||||
EmitReadComplex(sb, p, a, tc, i + " ", enableRefHandling);
|
||||
break;
|
||||
|
||||
case PropertyTypeKind.Collection:
|
||||
EmitReadCollection(sb, p, a, tc, i + " ", enableInternString);
|
||||
EmitReadCollection(sb, p, a, tc, i + " ", enableInternString, enableRefHandling);
|
||||
break;
|
||||
|
||||
case PropertyTypeKind.Dictionary:
|
||||
EmitReadDictionary(sb, p, a, tc, i + " ", enableInternString);
|
||||
EmitReadDictionary(sb, p, a, tc, i + " ", enableInternString, enableRefHandling);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
|
@ -267,7 +267,7 @@ public partial class AcBinarySourceGenerator
|
|||
/// Non-nullable + no ref → ZERO branches (tc consumed but ignored).
|
||||
/// No SGen → runtime fallback via ReadValueGenerated.
|
||||
/// </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)
|
||||
{
|
||||
|
|
@ -292,7 +292,11 @@ public partial class AcBinarySourceGenerator
|
|||
var reader = p.WriterClassName!.Replace("_GeneratedWriter", "_GeneratedReader");
|
||||
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
|
||||
// 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.
|
||||
/// Else → runtime fallback via ReadValueGenerated.
|
||||
/// </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
|
||||
if (p.CollectionKind != null && CanInlineCollectionRead(p))
|
||||
{
|
||||
EmitReadCollectionInline(sb, p, a, tc, i, enableInternString);
|
||||
EmitReadCollectionInline(sb, p, a, tc, i, enableInternString, enableRefHandling);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -415,7 +419,7 @@ public partial class AcBinarySourceGenerator
|
|||
/// Wire format: [Dictionary][VarUInt count][key₁ value₁ key₂ value₂ ...].
|
||||
/// Keys and values are read inline when their types are known (primitive/string/Complex+SGen).
|
||||
/// </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 keyType = p.DictKeyTypeName ?? "object";
|
||||
|
|
@ -462,16 +466,23 @@ public partial class AcBinarySourceGenerator
|
|||
sb.AppendLine($"{i} {valReader}.Instance.ReadProperties(rv_{s}, context);");
|
||||
sb.AppendLine($"{i} dv_{s} = rv_{s};");
|
||||
sb.AppendLine($"{i} }}");
|
||||
sb.AppendLine($"{i} else if ({vtc} == BinaryTypeCode.ObjectRefFirst)");
|
||||
sb.AppendLine($"{i} {{");
|
||||
sb.AppendLine($"{i} var rci_{s} = (int)context.ReadVarUInt();");
|
||||
sb.AppendLine($"{i} var rv_{s} = new {valType}();");
|
||||
sb.AppendLine($"{i} context.RegisterInternedValueAt(rci_{s}, rv_{s});");
|
||||
sb.AppendLine($"{i} {valReader}.Instance.ReadProperties(rv_{s}, context);");
|
||||
sb.AppendLine($"{i} dv_{s} = rv_{s};");
|
||||
sb.AppendLine($"{i} }}");
|
||||
sb.AppendLine($"{i} else if ({vtc} == BinaryTypeCode.ObjectRef)");
|
||||
sb.AppendLine($"{i} dv_{s} = ({valType})context.GetInternedObject((int)context.ReadVarUInt())!;");
|
||||
// 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} {{");
|
||||
sb.AppendLine($"{i} var rci_{s} = (int)context.ReadVarUInt();");
|
||||
sb.AppendLine($"{i} var rv_{s} = new {valType}();");
|
||||
sb.AppendLine($"{i} context.RegisterInternedValueAt(rci_{s}, rv_{s});");
|
||||
sb.AppendLine($"{i} {valReader}.Instance.ReadProperties(rv_{s}, context);");
|
||||
sb.AppendLine($"{i} dv_{s} = rv_{s};");
|
||||
sb.AppendLine($"{i} }}");
|
||||
sb.AppendLine($"{i} else if ({vtc} == BinaryTypeCode.ObjectRef)");
|
||||
sb.AppendLine($"{i} dv_{s} = ({valType})context.GetInternedObject((int)context.ReadVarUInt())!;");
|
||||
}
|
||||
sb.AppendLine($"{i} else if ({vtc} != BinaryTypeCode.Null)");
|
||||
sb.AppendLine($"{i} {{");
|
||||
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).
|
||||
/// Eliminates per-element: ReadValue dispatch, ReadObjectCore dict lookup, Activator.CreateInstance.
|
||||
/// </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 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} {{");
|
||||
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
|
||||
EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: true, null, enableInternString);
|
||||
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} {{");
|
||||
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
|
||||
EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: false, p.CollectionAddMethod, enableInternString);
|
||||
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} {{");
|
||||
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
|
||||
EmitReadNonComplexCollectionElement(sb, p, $"ri_{s}", s, i + " ", isArray: false, null, enableInternString);
|
||||
sb.AppendLine($"{i} }}");
|
||||
|
|
@ -663,7 +674,7 @@ public partial class AcBinarySourceGenerator
|
|||
/// SGen reader = non-metadata mode → no ObjectWithMetadata fallback.
|
||||
/// !needsRefScan → only Object/Null possible → 1 branch per element.
|
||||
/// </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}";
|
||||
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 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
|
||||
// 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.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.Build.0 = 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|x86.ActiveCfg = Release|Any CPU
|
||||
|
|
|
|||
|
|
@ -34,8 +34,51 @@ namespace AyCode.Core.Serializers.Attributes;
|
|||
public sealed class AcBinarySerializableAttribute : Attribute
|
||||
{
|
||||
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; }
|
||||
|
||||
/// <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; }
|
||||
|
||||
/// <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; }
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Reference in New Issue