[LOADED_DOCS: 2 files, no new loads]
Refactor: centralize config/state in Configuration.cs Moved all benchmark configuration, mutable state, and attribute-flag aggregation from Program.cs to a new Configuration.cs static class. Updated all references in Program.cs and related benchmark classes to use Configuration.<value>. Removed the "profiler" CLI mode and its code. Updated README.md to reflect these changes. This improves maintainability and keeps Program.cs focused on orchestration and UX, with no changes to benchmark logic.
This commit is contained in:
parent
46b26b7238
commit
eb3185c78d
|
|
@ -0,0 +1,133 @@
|
||||||
|
using AyCode.Core.Serializers.Attributes;
|
||||||
|
using AyCode.Core.Serializers.Binaries;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace AyCode.Core.Serializers.Console;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration state for the benchmark application. Holds compile-time and runtime constants,
|
||||||
|
/// per-run mutable settings (WireMode, charset, iteration counts), and the attribute-flag
|
||||||
|
/// aggregation that drives the per-row Options column. Split out from <c>Program.cs</c> so the
|
||||||
|
/// entry-point file can focus on UX-flow and benchmark orchestration; everything in this class
|
||||||
|
/// is config / state — no benchmark logic. Single instance (static class) — the application is
|
||||||
|
/// a one-shot process, no multi-tenant state isolation needed.
|
||||||
|
/// </summary>
|
||||||
|
internal static class Configuration
|
||||||
|
{
|
||||||
|
internal const string ResultsDirectory = @"H:\Applications\Aycode\Source\AyCode.Core\Test_Benchmark_Results\Benchmark";
|
||||||
|
|
||||||
|
#if AYCODE_NATIVEAOT
|
||||||
|
internal const string BuildConfiguration = "NativeAOT";
|
||||||
|
#elif DEBUG
|
||||||
|
internal const string BuildConfiguration = "Debug";
|
||||||
|
#else
|
||||||
|
internal const string BuildConfiguration = "Release";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
internal static int WarmupIterations = 0;
|
||||||
|
internal static int TestIterations = 1;
|
||||||
|
internal static int BenchmarkSamples = 1; // Debug: single sample, fast iteration
|
||||||
|
#else
|
||||||
|
internal static int WarmupIterations = 5000; //10000 — per-phase (Ser AND Des get their own warmup separately)
|
||||||
|
internal static int TestIterations = 1000; //1000
|
||||||
|
internal static int BenchmarkSamples = 10;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Interactive settings: selected AcBinary wire mode for benchmark runs.
|
||||||
|
// 1 = Compact, 2 = Fast
|
||||||
|
internal static WireMode SelectedWireMode = WireMode.Compact;
|
||||||
|
|
||||||
|
// Serializer name constants
|
||||||
|
// Engine identifiers (used in Engine column + comparison logic)
|
||||||
|
internal const string EngineAcBinary = "AcBinary";
|
||||||
|
internal const string EngineMemoryPack = "MemoryPack";
|
||||||
|
#if !AYCODE_NATIVEAOT
|
||||||
|
internal const string EngineMessagePack = "MessagePack";
|
||||||
|
#endif
|
||||||
|
internal const string EngineSystemTextJson = "System.Text.Json";
|
||||||
|
|
||||||
|
// IO mode identifiers (used in IO column + comparison logic)
|
||||||
|
internal const string IoByteArray = "Byte[]";
|
||||||
|
internal const string IoBufWrReuse = "BufWr reuse";
|
||||||
|
internal const string IoBufWrNew = "BufWr new";
|
||||||
|
internal const string IoString = "String";
|
||||||
|
internal const string IoNamedPipe = "NamedPipe";
|
||||||
|
internal const string IoNamedPipeRaw = "NamedPipe";
|
||||||
|
internal const string IoInMemoryPipe = "Pipe(in-mem)";
|
||||||
|
internal const string IoInMemoryRaw = "Pipe(in-mem)";
|
||||||
|
|
||||||
|
// Single source of truth for the chunk size used by ALL pipe-related benchmarks (NamedPipe PipeChunk,
|
||||||
|
// NamedPipe PipeRaw, in-memory Pipe, in-memory RawMem) AND the NamedPipe server's inBufferSize/outBufferSize.
|
||||||
|
// Same value across both layers ensures apples-to-apples comparison: chunked-streaming chunk-on-wire size
|
||||||
|
// matches the kernel pipe-buffer slot exactly. Tweak HERE when experimenting; do NOT scatter chunkSize
|
||||||
|
// overrides across individual benchmark rows.
|
||||||
|
internal const int PipeChunkSize = 4096;
|
||||||
|
|
||||||
|
// Dispatch mode identifiers — describes how property access / type dispatch happens for a given run.
|
||||||
|
// SGen = compile-time source generator path (Unsafe.As<T> direct fields, slot-array wrapper lookup).
|
||||||
|
// Runtime= reflection / compiled-delegate path.
|
||||||
|
// Hybrid = SGen root with non-SGen child types reached via bridge methods. See docs/BINARY/BINARY_SGEN.md.
|
||||||
|
internal const string ModeSGen = "SGen";
|
||||||
|
internal const string ModeRuntime = "Runtime";
|
||||||
|
internal const string ModeHybrid = "Hybrid";
|
||||||
|
|
||||||
|
// Per-cell adaptive iteration target wall-clock duration. Each Ser/Des function calibrates its
|
||||||
|
// own iteration count post-warmup so the sample batch lands in this range — equalizes the
|
||||||
|
// per-sample window across cells of vastly different per-op cost (Small ~6 ns/op vs Large
|
||||||
|
// ~140 µs/op). Below ~100 ms Stopwatch precision and OS preempt spikes start to dominate.
|
||||||
|
internal const int TargetSampleMs = 250;
|
||||||
|
|
||||||
|
// CV (coefficient of variation = stddev / mean) threshold above which a row's range is flagged
|
||||||
|
// as "unstable" in the markdown output (⚠️ marker). 3% is a reasonable noise-floor expectation
|
||||||
|
// for stabilized in-memory benchmarks; rows above it should be discounted when reading
|
||||||
|
// sub-3% inter-engine deltas.
|
||||||
|
internal const double UnstableCVThreshold = 0.03;
|
||||||
|
|
||||||
|
// JIT-tier-promotion drain delay between warmup and measurement.
|
||||||
|
// - JIT mode (RuntimeFeature.IsDynamicCodeCompiled == true): tiered JIT promotes hot methods
|
||||||
|
// in a background thread; we wait briefly for the queue to drain so the first measurement
|
||||||
|
// sample doesn't catch a Tier-0 → Tier-1 transition mid-flight.
|
||||||
|
// - AOT mode (NativeAOT publish): no dynamic compilation happens; the sleep is pure noise.
|
||||||
|
// 250ms (vs the historical 3000ms) is sufficient for a few-method working set under .NET 9's
|
||||||
|
// tiered JIT — empirically the queue drains in <100ms for the bench's hot path.
|
||||||
|
internal static int JitSleep => RuntimeFeature.IsDynamicCodeCompiled ? 250 : 0;
|
||||||
|
|
||||||
|
// OptionsPreset values are passed per-instance (constructor argument), not constants —
|
||||||
|
// each CreateSerializers call line specifies its own preset name (e.g. "FastMode", "NoIntern").
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,120 +32,7 @@ namespace AyCode.Core.Serializers.Console;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Program
|
public static class Program
|
||||||
{
|
{
|
||||||
private const string ResultsDirectory = @"H:\Applications\Aycode\Source\AyCode.Core\Test_Benchmark_Results\Benchmark";
|
// Configuration (constants, mutable state, attribute-flag aggregation) → Configuration.cs
|
||||||
|
|
||||||
#if AYCODE_NATIVEAOT
|
|
||||||
private const string BuildConfiguration = "NativeAOT";
|
|
||||||
#elif DEBUG
|
|
||||||
private const string BuildConfiguration = "Debug";
|
|
||||||
#else
|
|
||||||
private const string BuildConfiguration = "Release";
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
private static int WarmupIterations = 0;
|
|
||||||
private static int TestIterations = 1;
|
|
||||||
private static int BenchmarkSamples = 1; // Debug: single sample, fast iteration
|
|
||||||
#else
|
|
||||||
private static int WarmupIterations = 5000; //10000 — per-phase (Ser AND Des get their own warmup separately)
|
|
||||||
private static int TestIterations = 1000; //1000
|
|
||||||
private static int BenchmarkSamples = 10;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Interactive settings: selected AcBinary wire mode for benchmark runs.
|
|
||||||
// 1 = Compact, 2 = Fast
|
|
||||||
private static WireMode SelectedWireMode = WireMode.Compact;
|
|
||||||
|
|
||||||
// Serializer name constants
|
|
||||||
// Engine identifiers (used in Engine column + comparison logic)
|
|
||||||
private const string EngineAcBinary = "AcBinary";
|
|
||||||
private const string EngineMemoryPack = "MemoryPack";
|
|
||||||
#if !AYCODE_NATIVEAOT
|
|
||||||
private const string EngineMessagePack = "MessagePack";
|
|
||||||
#endif
|
|
||||||
private const string EngineSystemTextJson = "System.Text.Json";
|
|
||||||
|
|
||||||
// IO mode identifiers (used in IO column + comparison logic)
|
|
||||||
private const string IoByteArray = "Byte[]";
|
|
||||||
private const string IoBufWrReuse = "BufWr reuse";
|
|
||||||
private const string IoBufWrNew = "BufWr new";
|
|
||||||
private const string IoString = "String";
|
|
||||||
private const string IoNamedPipe = "NamedPipe";
|
|
||||||
private const string IoNamedPipeRaw = "NamedPipe";
|
|
||||||
private const string IoInMemoryPipe = "Pipe(in-mem)";
|
|
||||||
private const string IoInMemoryRaw = "Pipe(in-mem)";
|
|
||||||
|
|
||||||
// Single source of truth for the chunk size used by ALL pipe-related benchmarks (NamedPipe PipeChunk,
|
|
||||||
// NamedPipe PipeRaw, in-memory Pipe, in-memory RawMem) AND the NamedPipe server's inBufferSize/outBufferSize.
|
|
||||||
// Same value across both layers ensures apples-to-apples comparison: chunked-streaming chunk-on-wire size
|
|
||||||
// matches the kernel pipe-buffer slot exactly. Tweak HERE when experimenting; do NOT scatter chunkSize
|
|
||||||
// overrides across individual benchmark rows.
|
|
||||||
private const int PipeChunkSize = 4096;
|
|
||||||
|
|
||||||
// Dispatch mode identifiers — describes how property access / type dispatch happens for a given run.
|
|
||||||
// SGen = compile-time source generator path (Unsafe.As<T> direct fields, slot-array wrapper lookup).
|
|
||||||
// Runtime= reflection / compiled-delegate path.
|
|
||||||
// Hybrid = SGen root with non-SGen child types reached via bridge methods. See docs/BINARY/BINARY_SGEN.md.
|
|
||||||
private const string ModeSGen = "SGen";
|
|
||||||
private const string ModeRuntime = "Runtime";
|
|
||||||
private const string ModeHybrid = "Hybrid";
|
|
||||||
|
|
||||||
// Per-cell adaptive iteration target wall-clock duration. Each Ser/Des function calibrates its
|
|
||||||
// own iteration count post-warmup so the sample batch lands in this range — equalizes the
|
|
||||||
// per-sample window across cells of vastly different per-op cost (Small ~6 ns/op vs Large
|
|
||||||
// ~140 µs/op). Below ~100 ms Stopwatch precision and OS preempt spikes start to dominate.
|
|
||||||
private const int TargetSampleMs = 250;
|
|
||||||
|
|
||||||
// CV (coefficient of variation = stddev / mean) threshold above which a row's range is flagged
|
|
||||||
// as "unstable" in the markdown output (⚠️ marker). 3% is a reasonable noise-floor expectation
|
|
||||||
// for stabilized in-memory benchmarks; rows above it should be discounted when reading
|
|
||||||
// sub-3% inter-engine deltas.
|
|
||||||
private const double UnstableCVThreshold = 0.03;
|
|
||||||
|
|
||||||
// JIT-tier-promotion drain delay between warmup and measurement.
|
|
||||||
// - JIT mode (RuntimeFeature.IsDynamicCodeCompiled == true): tiered JIT promotes hot methods
|
|
||||||
// in a background thread; we wait briefly for the queue to drain so the first measurement
|
|
||||||
// sample doesn't catch a Tier-0 → Tier-1 transition mid-flight.
|
|
||||||
// - AOT mode (NativeAOT publish): no dynamic compilation happens; the sleep is pure noise.
|
|
||||||
// 250ms (vs the historical 3000ms) is sufficient for a few-method working set under .NET 9's
|
|
||||||
// tiered JIT — empirically the queue drains in <100ms for the bench's hot path.
|
|
||||||
private static int JitSleep => RuntimeFeature.IsDynamicCodeCompiled ? 250 : 0;
|
|
||||||
|
|
||||||
// OptionsPreset values are passed per-instance (constructor argument), not constants —
|
|
||||||
// each CreateSerializers call line specifies its own preset name (e.g. "FastMode", "NoIntern").
|
|
||||||
|
|
||||||
private 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 <see cref="BuildAcBinaryOptionsDescription"/> 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>
|
|
||||||
private static readonly (bool refHandling, bool internString, bool metadata, bool idTracking, bool propertyFilter) _attrFlags
|
|
||||||
= ScanAcBinaryAttributeFlags();
|
|
||||||
|
|
||||||
private static (bool refHandling, bool internString, bool metadata, bool idTracking, bool propertyFilter) ScanAcBinaryAttributeFlags()
|
|
||||||
{
|
|
||||||
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>
|
||||||
/// Common Options-column formatter for every AcBinary serializer benchmark row. Renders the
|
/// Common Options-column formatter for every AcBinary serializer benchmark row. Renders the
|
||||||
|
|
@ -163,16 +50,16 @@ public static class Program
|
||||||
var propFilterOpt = options.PropertyFilter == null ? "None" : "Set";
|
var propFilterOpt = options.PropertyFilter == null ? "None" : "Set";
|
||||||
|
|
||||||
return $"WireMode={options.WireMode}, " +
|
return $"WireMode={options.WireMode}, " +
|
||||||
$"RefHandling={options.ReferenceHandling}(opt) | {_attrFlags.refHandling} (attr), " +
|
$"RefHandling={options.ReferenceHandling}(opt) | {Configuration.AttrFlags.refHandling} (attr), " +
|
||||||
$"Interning={options.UseStringInterning}(opt) | {_attrFlags.internString} (attr), " +
|
$"Interning={options.UseStringInterning}(opt) | {Configuration.AttrFlags.internString} (attr), " +
|
||||||
$"Metadata={options.UseMetadata}(opt) | {_attrFlags.metadata} (attr), " +
|
$"Metadata={options.UseMetadata}(opt) | {Configuration.AttrFlags.metadata} (attr), " +
|
||||||
$"PropertyFilter={propFilterOpt}(opt) | {_attrFlags.propertyFilter} (attr), " +
|
$"PropertyFilter={propFilterOpt}(opt) | {Configuration.AttrFlags.propertyFilter} (attr), " +
|
||||||
$"SGen={options.UseGeneratedCode}, " +
|
$"SGen={options.UseGeneratedCode}, " +
|
||||||
$"Compression={options.UseCompression}{extra}";
|
$"Compression={options.UseCompression}{extra}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns MemoryPack serializer options aligned with <see cref="SelectedWireMode"/> for a fair
|
/// Returns MemoryPack serializer options aligned with <see cref="Configuration.SelectedWireMode"/> for a fair
|
||||||
/// apples-to-apples wire-format comparison:
|
/// apples-to-apples wire-format comparison:
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item><see cref="WireMode.Compact"/> → <see cref="MemoryPackSerializerOptions.Default"/> (UTF-8) — both
|
/// <item><see cref="WireMode.Compact"/> → <see cref="MemoryPackSerializerOptions.Default"/> (UTF-8) — both
|
||||||
|
|
@ -185,15 +72,15 @@ public static class Program
|
||||||
/// the encoding-family difference, NOT an AcBinary-specific overhead.
|
/// the encoding-family difference, NOT an AcBinary-specific overhead.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static MemoryPackSerializerOptions GetMemPackOptions() =>
|
private static MemoryPackSerializerOptions GetMemPackOptions() =>
|
||||||
SelectedWireMode == WireMode.Fast
|
Configuration.SelectedWireMode == WireMode.Fast
|
||||||
? MemoryPackSerializerOptions.Utf16
|
? MemoryPackSerializerOptions.Utf16
|
||||||
: MemoryPackSerializerOptions.Default;
|
: MemoryPackSerializerOptions.Default;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts a total-time (in ms across <see cref="TestIterations"/>) into per-operation microseconds.
|
/// Converts a total-time (in ms across <see cref="Configuration.TestIterations"/>) into per-operation microseconds.
|
||||||
/// Formula: <c>totalMs / iterations × 1000</c>. The benchmark stores <c>*TimeMs</c> as the cumulative
|
/// Formula: <c>totalMs / iterations × 1000</c>. The benchmark stores <c>*TimeMs</c> as the cumulative
|
||||||
/// median over the timing run; the display layer renders per-op µs to make numbers iteration-count
|
/// median over the timing run; the display layer renders per-op µs to make numbers iteration-count
|
||||||
/// independent (e.g. switching <c>TestIterations</c> 1000 → 100 leaves the displayed µs/op unchanged
|
/// independent (e.g. switching <c>Configuration.TestIterations</c> 1000 → 100 leaves the displayed µs/op unchanged
|
||||||
/// — only its sample noise grows). Symmetric with the already-per-op <c>*AllocBytesPerOp</c> fields.
|
/// — only its sample noise grows). Symmetric with the already-per-op <c>*AllocBytesPerOp</c> fields.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
|
@ -202,7 +89,7 @@ public static class Program
|
||||||
/// Converts a total-time (in ms across <paramref name="iterations"/>) into per-operation microseconds.
|
/// Converts a total-time (in ms across <paramref name="iterations"/>) into per-operation microseconds.
|
||||||
/// Per-op µs is the iter-independent unit: 1000 iter and 50000 iter of the same operation should
|
/// Per-op µs is the iter-independent unit: 1000 iter and 50000 iter of the same operation should
|
||||||
/// produce the same per-op µs (within noise). Necessary because per-cell adaptive iteration makes
|
/// produce the same per-op µs (within noise). Necessary because per-cell adaptive iteration makes
|
||||||
/// <c>iterations</c> a per-row property — there is no longer a single global TestIterations to divide by.
|
/// <c>iterations</c> a per-row property — there is no longer a single global Configuration.TestIterations to divide by.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static double ToPerOpMicros(double totalMs, int iterations) => iterations > 0 ? totalMs / iterations * 1000.0 : 0;
|
private static double ToPerOpMicros(double totalMs, int iterations) => iterations > 0 ? totalMs / iterations * 1000.0 : 0;
|
||||||
|
|
||||||
|
|
@ -271,7 +158,7 @@ public static class Program
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Formats a per-op micros value with its inter-sample range and CV-threshold marker as
|
/// Formats a per-op micros value with its inter-sample range and CV-threshold marker as
|
||||||
/// <c>"26.86 (24.5..29.1)"</c> or <c>"26.86 (24.5..29.1) ⚠️5.2%"</c>. Median first, range in parentheses,
|
/// <c>"26.86 (24.5..29.1)"</c> or <c>"26.86 (24.5..29.1) ⚠️5.2%"</c>. Median first, range in parentheses,
|
||||||
/// CV warning suffix only when CV > <see cref="UnstableCVThreshold"/>. When min == max == median
|
/// CV warning suffix only when CV > <see cref="Configuration.UnstableCVThreshold"/>. When min == max == median
|
||||||
/// (single-sample / Debug / quick mode), collapses to bare median to avoid visual clutter.
|
/// (single-sample / Debug / quick mode), collapses to bare median to avoid visual clutter.
|
||||||
/// All time inputs are total-batch milliseconds; <paramref name="iterations"/> is the per-row iter
|
/// All time inputs are total-batch milliseconds; <paramref name="iterations"/> is the per-row iter
|
||||||
/// count (post-adaptive-calibration).
|
/// count (post-adaptive-calibration).
|
||||||
|
|
@ -292,7 +179,7 @@ public static class Program
|
||||||
if (medianMs > 0 && stdDevMs > 0)
|
if (medianMs > 0 && stdDevMs > 0)
|
||||||
{
|
{
|
||||||
var cv = stdDevMs / medianMs;
|
var cv = stdDevMs / medianMs;
|
||||||
if (cv > UnstableCVThreshold)
|
if (cv > Configuration.UnstableCVThreshold)
|
||||||
{
|
{
|
||||||
var cvPct = (cv * 100).ToString("F1", inv);
|
var cvPct = (cv * 100).ToString("F1", inv);
|
||||||
return $"{range} ⚠️{cvPct}%";
|
return $"{range} ⚠️{cvPct}%";
|
||||||
|
|
@ -324,7 +211,7 @@ public static class Program
|
||||||
if (args.Length > 0)
|
if (args.Length > 0)
|
||||||
{
|
{
|
||||||
if (!TryParseCliArgs(args, out var layer, out var opMode, out var serializerMode))
|
if (!TryParseCliArgs(args, out var layer, out var opMode, out var serializerMode))
|
||||||
return; // profiler mode (already ran) or invalid args
|
return; // invalid args
|
||||||
|
|
||||||
RunBenchmark(layer, opMode, serializerMode);
|
RunBenchmark(layer, opMode, serializerMode);
|
||||||
return;
|
return;
|
||||||
|
|
@ -350,8 +237,7 @@ public static class Program
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parses CLI arguments into (layer, opMode, serializerMode). Returns <c>false</c> if the args
|
/// Parses CLI arguments into (layer, opMode, serializerMode). Returns <c>false</c> if the args
|
||||||
/// indicate a special mode that has already been handled (e.g. <c>profiler</c>) or are invalid;
|
/// are invalid; the caller should then exit without running the standard benchmark.
|
||||||
/// the caller should then exit without running the standard benchmark.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool TryParseCliArgs(string[] args, out string layer, out string opMode, out string serializerMode)
|
private static bool TryParseCliArgs(string[] args, out string layer, out string opMode, out string serializerMode)
|
||||||
{
|
{
|
||||||
|
|
@ -361,19 +247,12 @@ public static class Program
|
||||||
|
|
||||||
var arg = args[0].ToLower();
|
var arg = args[0].ToLower();
|
||||||
|
|
||||||
// Profiler mode: warmup only, then exit (for memory profiler analysis)
|
|
||||||
if (arg == "profiler")
|
|
||||||
{
|
|
||||||
RunProfilerMode();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quick mode: short warmup, few iterations, small sample count
|
// Quick mode: short warmup, few iterations, small sample count
|
||||||
if (arg == "quick")
|
if (arg == "quick")
|
||||||
{
|
{
|
||||||
WarmupIterations = 5;
|
Configuration.WarmupIterations = 5;
|
||||||
TestIterations = 100;
|
Configuration.TestIterations = 100;
|
||||||
BenchmarkSamples = 3;
|
Configuration.BenchmarkSamples = 3;
|
||||||
layer = "all";
|
layer = "all";
|
||||||
}
|
}
|
||||||
else if (arg is "core" or "comprehensive" or "edge" or "all"
|
else if (arg is "core" or "comprehensive" or "edge" or "all"
|
||||||
|
|
@ -424,7 +303,7 @@ public static class Program
|
||||||
// randomly inflate samples by 5-15%.
|
// randomly inflate samples by 5-15%.
|
||||||
// Try/finally guarantees the original state is restored even if a benchmark throws — leaving
|
// Try/finally guarantees the original state is restored even if a benchmark throws — leaving
|
||||||
// a developer machine pinned to one core after a crashed run is a real foot-gun.
|
// a developer machine pinned to one core after a crashed run is a real foot-gun.
|
||||||
// Skipped on Debug single-sample mode (BenchmarkSamples <= 1) where stabilization is moot.
|
// Skipped on Debug single-sample mode (Configuration.BenchmarkSamples <= 1) where stabilization is moot.
|
||||||
var process = Process.GetCurrentProcess();
|
var process = Process.GetCurrentProcess();
|
||||||
var origAffinity = (IntPtr)0;
|
var origAffinity = (IntPtr)0;
|
||||||
var origPriority = ProcessPriorityClass.Normal;
|
var origPriority = ProcessPriorityClass.Normal;
|
||||||
|
|
@ -433,7 +312,7 @@ public static class Program
|
||||||
// ProcessorAffinity is only supported on Windows + Linux (CA1416). macOS would throw at
|
// ProcessorAffinity is only supported on Windows + Linux (CA1416). macOS would throw at
|
||||||
// runtime; skip the affinity step there but still raise priority class (which IS supported
|
// runtime; skip the affinity step there but still raise priority class (which IS supported
|
||||||
// on macOS, just less effective for stabilization than affinity pinning).
|
// on macOS, just less effective for stabilization than affinity pinning).
|
||||||
if (BenchmarkSamples > 1 && (OperatingSystem.IsWindows() || OperatingSystem.IsLinux()))
|
if (Configuration.BenchmarkSamples > 1 && (OperatingSystem.IsWindows() || OperatingSystem.IsLinux()))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -465,8 +344,8 @@ public static class Program
|
||||||
var allTestDataSets = BenchmarkTestDataProvider.CreateTestDataSets();
|
var allTestDataSets = BenchmarkTestDataProvider.CreateTestDataSets();
|
||||||
var testDataSets = FilterByLayer(allTestDataSets, layer);
|
var testDataSets = FilterByLayer(allTestDataSets, layer);
|
||||||
|
|
||||||
System.Console.WriteLine($"Layer: {layer} | OpMode: {opMode} | SerializerMode: {serializerMode} | Charset: {GetCurrentCharsetName()} | Iterations: per-cell adaptive (~{TargetSampleMs} ms target) | Warmup: {WarmupIterations} per phase (Ser/Des isolated) | Samples: {BenchmarkSamples} (median) + pilot discard");
|
System.Console.WriteLine($"Layer: {layer} | OpMode: {opMode} | SerializerMode: {serializerMode} | Charset: {GetCurrentCharsetName()} | Iterations: per-cell adaptive (~{Configuration.TargetSampleMs} ms target) | Warmup: {Configuration.WarmupIterations} per phase (Ser/Des isolated) | Samples: {Configuration.BenchmarkSamples} (median) + pilot discard");
|
||||||
System.Console.WriteLine($"Build: {BuildConfiguration} | .NET: {Environment.Version} | Test Type: {testDataSets.FirstOrDefault()?.TypeName ?? "unknown"} | Test Cells: {testDataSets.Count}/{allTestDataSets.Count}");
|
System.Console.WriteLine($"Build: {Configuration.BuildConfiguration} | .NET: {Environment.Version} | Test Type: {testDataSets.FirstOrDefault()?.TypeName ?? "unknown"} | Test Cells: {testDataSets.Count}/{allTestDataSets.Count}");
|
||||||
System.Console.WriteLine();
|
System.Console.WriteLine();
|
||||||
|
|
||||||
// Global JIT pre-warmup — touches every (testdata × serializer) code path BEFORE any timing happens.
|
// Global JIT pre-warmup — touches every (testdata × serializer) code path BEFORE any timing happens.
|
||||||
|
|
@ -476,7 +355,7 @@ public static class Program
|
||||||
// on later cells (e.g. Small BufWr reuse 9ms vs Medium BufWr reuse 4ms — even though Medium is bigger).
|
// on later cells (e.g. Small BufWr reuse 9ms vs Medium BufWr reuse 4ms — even though Medium is bigger).
|
||||||
// Pre-warmup runs every overload at least once with each data shape so .NET 9's tiered JIT promotes
|
// Pre-warmup runs every overload at least once with each data shape so .NET 9's tiered JIT promotes
|
||||||
// them all in the background; the per-cell warmup that follows then locks in cache + branch state.
|
// them all in the background; the per-cell warmup that follows then locks in cache + branch state.
|
||||||
if (BenchmarkSamples > 1) // skip in DEBUG (single-sample fast iteration)
|
if (Configuration.BenchmarkSamples > 1) // skip in DEBUG (single-sample fast iteration)
|
||||||
{
|
{
|
||||||
System.Console.WriteLine($"Global JIT pre-warmup ({testDataSets.Count} cells × all serializers, light pass)...");
|
System.Console.WriteLine($"Global JIT pre-warmup ({testDataSets.Count} cells × all serializers, light pass)...");
|
||||||
|
|
||||||
|
|
@ -500,7 +379,7 @@ public static class Program
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let background tiered-JIT compilation drain before we begin measuring.
|
// Let background tiered-JIT compilation drain before we begin measuring.
|
||||||
if (JitSleep > 0) Thread.Sleep(JitSleep);
|
if (Configuration.JitSleep > 0) Thread.Sleep(Configuration.JitSleep);
|
||||||
System.Console.WriteLine("✓ Global pre-warmup complete.\n");
|
System.Console.WriteLine("✓ Global pre-warmup complete.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -536,62 +415,6 @@ public static class Program
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Profiler mode: warmup only, then EXIT immediately.
|
|
||||||
/// Usage: dotnet run -- profiler
|
|
||||||
/// </summary>
|
|
||||||
private static void RunProfilerMode()
|
|
||||||
{
|
|
||||||
System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════╗");
|
|
||||||
System.Console.WriteLine("║ PROFILER MODE (AcBinary only) ║");
|
|
||||||
System.Console.WriteLine("╚══════════════════════════════════════════════════════════════════════╝");
|
|
||||||
System.Console.WriteLine($"Build: {BuildConfiguration} | .NET: {Environment.Version}");
|
|
||||||
System.Console.WriteLine();
|
|
||||||
|
|
||||||
var order = BenchmarkTestDataProvider.CreateProfilerOrder();
|
|
||||||
|
|
||||||
var options = AcBinarySerializerOptions.WithoutReferenceHandling;
|
|
||||||
options.UseStringInterning = StringInterningMode.None;
|
|
||||||
|
|
||||||
var bytes = AcBinarySerializer.Serialize(order, options);
|
|
||||||
|
|
||||||
// Warmup (fills caches)
|
|
||||||
System.Console.WriteLine("Warming up (1000 iterations)...");
|
|
||||||
for (var i = 0; i < 1000; i++)
|
|
||||||
{
|
|
||||||
_ = AcBinarySerializer.Serialize(order, options);
|
|
||||||
_ = AcBinaryDeserializer.Deserialize<TestOrder>(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread.Sleep(2000);
|
|
||||||
System.Console.WriteLine("Warmup complete. Caches are now populated.");
|
|
||||||
System.Console.WriteLine();
|
|
||||||
|
|
||||||
// HOT PATH - this is what the profiler should capture!
|
|
||||||
System.Console.WriteLine("Running hot path serialization (1000 iterations for profiling)...");
|
|
||||||
for (var i = 0; i < 1000; i++)
|
|
||||||
{
|
|
||||||
_ = AcBinarySerializer.Serialize(order, options);
|
|
||||||
//_ = AcBinaryDeserializer.Deserialize<TestOrder>(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
System.Console.WriteLine("Running hot path deserialization (1000 iterations for profiling)...");
|
|
||||||
for (var i = 0; i < 1000; i++)
|
|
||||||
{
|
|
||||||
_ = AcBinaryDeserializer.Deserialize<TestOrder>(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
System.Console.WriteLine("Hot path complete.");
|
|
||||||
System.Console.WriteLine();
|
|
||||||
|
|
||||||
System.Console.WriteLine(">>> ATTACH MEMORY PROFILER NOW <<<");
|
|
||||||
System.Console.WriteLine("Press any key to exit...");
|
|
||||||
System.Console.ReadKey(intercept: true);
|
|
||||||
|
|
||||||
System.Console.WriteLine();
|
|
||||||
System.Console.WriteLine("✓ Profiler mode complete. Exiting now.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Benchmark Execution
|
#region Benchmark Execution
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -644,9 +467,9 @@ public static class Program
|
||||||
// pool churn from Ser, deserialized object graph from Des) so the next phase starts with a quiescent
|
// pool churn from Ser, deserialized object graph from Des) so the next phase starts with a quiescent
|
||||||
// heap — GC tier-promotion timing during measurement is then driven only by THAT phase's allocations.
|
// heap — GC tier-promotion timing during measurement is then driven only by THAT phase's allocations.
|
||||||
//
|
//
|
||||||
// JitSleep per-phase: tiered JIT background promotion drain after each warmup (mode-aware: 0 ms in AOT).
|
// Configuration.JitSleep per-phase: tiered JIT background promotion drain after each warmup (mode-aware: 0 ms in AOT).
|
||||||
// Each phase's freshly-promoted methods settle before its timing starts.
|
// Each phase's freshly-promoted methods settle before its timing starts.
|
||||||
System.Console.WriteLine($"Running benchmarks (target ~{TargetSampleMs} ms/sample × {BenchmarkSamples} samples median, phase-isolated warmup/measure per Ser/Des)...\n");
|
System.Console.WriteLine($"Running benchmarks (target ~{Configuration.TargetSampleMs} ms/sample × {Configuration.BenchmarkSamples} samples median, phase-isolated warmup/measure per Ser/Des)...\n");
|
||||||
|
|
||||||
foreach (var serializer in serializers)
|
foreach (var serializer in serializers)
|
||||||
{
|
{
|
||||||
|
|
@ -676,10 +499,10 @@ public static class Program
|
||||||
if (mode is "all" or "serialize" or "ser")
|
if (mode is "all" or "serialize" or "ser")
|
||||||
{
|
{
|
||||||
ForceGcCollect();
|
ForceGcCollect();
|
||||||
serializer.WarmupSerialize(WarmupIterations);
|
serializer.WarmupSerialize(Configuration.WarmupIterations);
|
||||||
if (JitSleep > 0) Thread.Sleep(JitSleep);
|
if (Configuration.JitSleep > 0) Thread.Sleep(Configuration.JitSleep);
|
||||||
|
|
||||||
var rtIter = CalibrateIterations(() => serializer.Serialize(), TargetSampleMs);
|
var rtIter = CalibrateIterations(() => serializer.Serialize(), Configuration.TargetSampleMs);
|
||||||
var (rtMed, rtMin, rtMax, rtStd) = RunTimed(() => serializer.Serialize(), rtIter, $"{groupLabel} [RT timing]");
|
var (rtMed, rtMin, rtMax, rtStd) = RunTimed(() => serializer.Serialize(), rtIter, $"{groupLabel} [RT timing]");
|
||||||
result.RoundTripTimeMs = rtMed;
|
result.RoundTripTimeMs = rtMed;
|
||||||
result.RoundTripTimeMinMs = rtMin;
|
result.RoundTripTimeMinMs = rtMin;
|
||||||
|
|
@ -694,14 +517,14 @@ public static class Program
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// ── Ser phase ── isolated warmup → JitSleep → calibrate → time → alloc; preceded by GC.Collect.
|
// ── Ser phase ── isolated warmup → Configuration.JitSleep → calibrate → time → alloc; preceded by GC.Collect.
|
||||||
if (mode is "all" or "serialize" or "ser")
|
if (mode is "all" or "serialize" or "ser")
|
||||||
{
|
{
|
||||||
ForceGcCollect();
|
ForceGcCollect();
|
||||||
serializer.WarmupSerialize(WarmupIterations);
|
serializer.WarmupSerialize(Configuration.WarmupIterations);
|
||||||
if (JitSleep > 0) Thread.Sleep(JitSleep);
|
if (Configuration.JitSleep > 0) Thread.Sleep(Configuration.JitSleep);
|
||||||
|
|
||||||
var serIter = CalibrateIterations(() => serializer.Serialize(), TargetSampleMs);
|
var serIter = CalibrateIterations(() => serializer.Serialize(), Configuration.TargetSampleMs);
|
||||||
var (serMed, serMin, serMax, serStd) = RunTimed(() => serializer.Serialize(), serIter, $"{groupLabel} [Ser timing]");
|
var (serMed, serMin, serMax, serStd) = RunTimed(() => serializer.Serialize(), serIter, $"{groupLabel} [Ser timing]");
|
||||||
result.SerializeTimeMs = serMed;
|
result.SerializeTimeMs = serMed;
|
||||||
result.SerializeTimeMinMs = serMin;
|
result.SerializeTimeMinMs = serMin;
|
||||||
|
|
@ -712,16 +535,16 @@ public static class Program
|
||||||
result.SerializeAllocBytesPerOp = MeasureAllocation(() => serializer.Serialize(), serIter, $"{groupLabel} [Ser alloc]");
|
result.SerializeAllocBytesPerOp = MeasureAllocation(() => serializer.Serialize(), serIter, $"{groupLabel} [Ser alloc]");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Des phase ── isolated warmup → JitSleep → calibrate → time → alloc; preceded by GC.Collect.
|
// ── Des phase ── isolated warmup → Configuration.JitSleep → calibrate → time → alloc; preceded by GC.Collect.
|
||||||
// The GC.Collect here is critical: it discards the Ser-phase's write-buffer pool churn so the
|
// The GC.Collect here is critical: it discards the Ser-phase's write-buffer pool churn so the
|
||||||
// Des-phase's allocation measurement reflects ONLY Des-side allocations (deserialized object graph).
|
// Des-phase's allocation measurement reflects ONLY Des-side allocations (deserialized object graph).
|
||||||
if (mode is "all" or "deserialize" or "des")
|
if (mode is "all" or "deserialize" or "des")
|
||||||
{
|
{
|
||||||
ForceGcCollect();
|
ForceGcCollect();
|
||||||
serializer.WarmupDeserialize(WarmupIterations);
|
serializer.WarmupDeserialize(Configuration.WarmupIterations);
|
||||||
if (JitSleep > 0) Thread.Sleep(JitSleep);
|
if (Configuration.JitSleep > 0) Thread.Sleep(Configuration.JitSleep);
|
||||||
|
|
||||||
var desIter = CalibrateIterations(() => serializer.Deserialize(), TargetSampleMs);
|
var desIter = CalibrateIterations(() => serializer.Deserialize(), Configuration.TargetSampleMs);
|
||||||
var (desMed, desMin, desMax, desStd) = RunTimed(() => serializer.Deserialize(), desIter, $"{groupLabel} [Des timing]");
|
var (desMed, desMin, desMax, desStd) = RunTimed(() => serializer.Deserialize(), desIter, $"{groupLabel} [Des timing]");
|
||||||
result.DeserializeTimeMs = desMed;
|
result.DeserializeTimeMs = desMed;
|
||||||
result.DeserializeTimeMinMs = desMin;
|
result.DeserializeTimeMinMs = desMin;
|
||||||
|
|
@ -767,7 +590,7 @@ public static class Program
|
||||||
if (serializerMode == "fastestbyte")
|
if (serializerMode == "fastestbyte")
|
||||||
{
|
{
|
||||||
var fastestByteOptions = AcBinarySerializerOptions.FastMode;
|
var fastestByteOptions = AcBinarySerializerOptions.FastMode;
|
||||||
fastestByteOptions.WireMode = SelectedWireMode;
|
fastestByteOptions.WireMode = Configuration.SelectedWireMode;
|
||||||
|
|
||||||
return new List<ISerializerBenchmark>
|
return new List<ISerializerBenchmark>
|
||||||
{
|
{
|
||||||
|
|
@ -790,8 +613,8 @@ public static class Program
|
||||||
// fits blocking-free in one kernel pipe-buffer slot. Single source of truth for both app-level
|
// fits blocking-free in one kernel pipe-buffer slot. Single source of truth for both app-level
|
||||||
// wire chunk AND kernel transfer unit; change ONLY this line when tuning.
|
// wire chunk AND kernel transfer unit; change ONLY this line when tuning.
|
||||||
var binaryFastModePipeChunkOnly = AcBinarySerializerOptions.FastMode;
|
var binaryFastModePipeChunkOnly = AcBinarySerializerOptions.FastMode;
|
||||||
binaryFastModePipeChunkOnly.BufferWriterChunkSize = PipeChunkSize;
|
binaryFastModePipeChunkOnly.BufferWriterChunkSize = Configuration.PipeChunkSize;
|
||||||
binaryFastModePipeChunkOnly.WireMode = SelectedWireMode;
|
binaryFastModePipeChunkOnly.WireMode = Configuration.SelectedWireMode;
|
||||||
|
|
||||||
return new List<ISerializerBenchmark>
|
return new List<ISerializerBenchmark>
|
||||||
{
|
{
|
||||||
|
|
@ -826,18 +649,18 @@ public static class Program
|
||||||
|
|
||||||
var binaryNoInternOption = AcBinarySerializerOptions.Default;
|
var binaryNoInternOption = AcBinarySerializerOptions.Default;
|
||||||
binaryNoInternOption.UseStringInterning = StringInterningMode.None;
|
binaryNoInternOption.UseStringInterning = StringInterningMode.None;
|
||||||
binaryNoInternOption.WireMode = SelectedWireMode;
|
binaryNoInternOption.WireMode = Configuration.SelectedWireMode;
|
||||||
|
|
||||||
var binaryDefaultNoSgenOption = AcBinarySerializerOptions.Default;
|
var binaryDefaultNoSgenOption = AcBinarySerializerOptions.Default;
|
||||||
binaryDefaultNoSgenOption.UseGeneratedCode = false;
|
binaryDefaultNoSgenOption.UseGeneratedCode = false;
|
||||||
binaryDefaultNoSgenOption.WireMode = SelectedWireMode;
|
binaryDefaultNoSgenOption.WireMode = Configuration.SelectedWireMode;
|
||||||
|
|
||||||
var binaryFastModeNoSgenOption = AcBinarySerializerOptions.FastMode;
|
var binaryFastModeNoSgenOption = AcBinarySerializerOptions.FastMode;
|
||||||
binaryFastModeNoSgenOption.UseGeneratedCode = false;
|
binaryFastModeNoSgenOption.UseGeneratedCode = false;
|
||||||
binaryFastModeNoSgenOption.WireMode = SelectedWireMode;
|
binaryFastModeNoSgenOption.WireMode = Configuration.SelectedWireMode;
|
||||||
|
|
||||||
var binaryFastModeOption = AcBinarySerializerOptions.FastMode;
|
var binaryFastModeOption = AcBinarySerializerOptions.FastMode;
|
||||||
binaryFastModeOption.WireMode = SelectedWireMode;
|
binaryFastModeOption.WireMode = Configuration.SelectedWireMode;
|
||||||
|
|
||||||
// BufWr new — 4 KB chunk size for the FRESH ArrayBufferWriter scenario. The chunkSize here drives
|
// BufWr new — 4 KB chunk size for the FRESH ArrayBufferWriter scenario. The chunkSize here drives
|
||||||
// the serializer's GetSpan(N) request → the ArrayBufferWriter's internal allocation per call.
|
// the serializer's GetSpan(N) request → the ArrayBufferWriter's internal allocation per call.
|
||||||
|
|
@ -845,20 +668,20 @@ public static class Program
|
||||||
// allocates a fresh ABW. Independent of the AsyncPipe profile (different mechanism: alloc overhead
|
// allocates a fresh ABW. Independent of the AsyncPipe profile (different mechanism: alloc overhead
|
||||||
// vs syscall count).
|
// vs syscall count).
|
||||||
var binaryFastModeBufWrChunk = AcBinarySerializerOptions.FastMode;
|
var binaryFastModeBufWrChunk = AcBinarySerializerOptions.FastMode;
|
||||||
binaryFastModeBufWrChunk.BufferWriterChunkSize = PipeChunkSize;
|
binaryFastModeBufWrChunk.BufferWriterChunkSize = Configuration.PipeChunkSize;
|
||||||
binaryFastModeBufWrChunk.WireMode = SelectedWireMode;
|
binaryFastModeBufWrChunk.WireMode = Configuration.SelectedWireMode;
|
||||||
|
|
||||||
// In-memory Pipe variant — same 4 KB chunkSize as the AsyncPipe mode, no kernel-pipe alignment
|
// In-memory Pipe variant — same 4 KB chunkSize as the AsyncPipe mode, no kernel-pipe alignment
|
||||||
// concern (managed slabs are not page-aligned anyway). Drives SerializeChunkedFramed via the in-memory
|
// concern (managed slabs are not page-aligned anyway). Drives SerializeChunkedFramed via the in-memory
|
||||||
// System.IO.Pipelines.Pipe (zero-copy slab handoff between producer and drain task).
|
// System.IO.Pipelines.Pipe (zero-copy slab handoff between producer and drain task).
|
||||||
var binaryFastModePipeChunkInMem = AcBinarySerializerOptions.FastMode;
|
var binaryFastModePipeChunkInMem = AcBinarySerializerOptions.FastMode;
|
||||||
binaryFastModePipeChunkInMem.BufferWriterChunkSize = PipeChunkSize;
|
binaryFastModePipeChunkInMem.BufferWriterChunkSize = Configuration.PipeChunkSize;
|
||||||
binaryFastModePipeChunkInMem.WireMode = SelectedWireMode;
|
binaryFastModePipeChunkInMem.WireMode = Configuration.SelectedWireMode;
|
||||||
|
|
||||||
var defaultOptions = AcBinarySerializerOptions.Default;
|
var defaultOptions = AcBinarySerializerOptions.Default;
|
||||||
defaultOptions.UseStringInterning = StringInterningMode.None;
|
defaultOptions.UseStringInterning = StringInterningMode.None;
|
||||||
defaultOptions.ReferenceHandling = ReferenceHandlingMode.OnlyId;
|
defaultOptions.ReferenceHandling = ReferenceHandlingMode.OnlyId;
|
||||||
defaultOptions.WireMode = SelectedWireMode;
|
defaultOptions.WireMode = Configuration.SelectedWireMode;
|
||||||
|
|
||||||
return new List<ISerializerBenchmark>
|
return new List<ISerializerBenchmark>
|
||||||
{
|
{
|
||||||
|
|
@ -927,10 +750,10 @@ public static class Program
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs the action <paramref name="iterations"/> times for <see cref="BenchmarkSamples"/> independent samples,
|
/// Runs the action <paramref name="iterations"/> times for <see cref="Configuration.BenchmarkSamples"/> independent samples,
|
||||||
/// returning the median, min, and max elapsed time. Multi-sample design reduces single-run variance
|
/// returning the median, min, and max elapsed time. Multi-sample design reduces single-run variance
|
||||||
/// from ~±15% to ~±5% by smoothing transient effects (background activity, thermal/turbo state).
|
/// from ~±15% to ~±5% by smoothing transient effects (background activity, thermal/turbo state).
|
||||||
/// When <see cref="BenchmarkSamples"/> <= 1, falls back to single-sample timing (Debug / quick mode).
|
/// When <see cref="Configuration.BenchmarkSamples"/> <= 1, falls back to single-sample timing (Debug / quick mode).
|
||||||
/// When <paramref name="progressLabel"/> is non-null, emits in-place <c>\r</c> progress updates so a
|
/// When <paramref name="progressLabel"/> is non-null, emits in-place <c>\r</c> progress updates so a
|
||||||
/// stuck benchmark (e.g. deadlocked NamedPipe row) is visibly stuck at a specific %% rather than
|
/// stuck benchmark (e.g. deadlocked NamedPipe row) is visibly stuck at a specific %% rather than
|
||||||
/// silently hanging.
|
/// silently hanging.
|
||||||
|
|
@ -948,7 +771,7 @@ public static class Program
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static (double medianMs, double minMs, double maxMs, double stdDevMs) RunTimed(Action action, int iterations, string? progressLabel = null)
|
private static (double medianMs, double minMs, double maxMs, double stdDevMs) RunTimed(Action action, int iterations, string? progressLabel = null)
|
||||||
{
|
{
|
||||||
var samples = BenchmarkSamples;
|
var samples = Configuration.BenchmarkSamples;
|
||||||
if (samples <= 1)
|
if (samples <= 1)
|
||||||
{
|
{
|
||||||
// Single-sample fast path (Debug or trivial run) — no allocation, no sort, no stddev.
|
// Single-sample fast path (Debug or trivial run) — no allocation, no sort, no stddev.
|
||||||
|
|
@ -1018,16 +841,16 @@ public static class Program
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per-cell adaptive iteration calibration. Runs a 100-iter measurement after warmup and computes
|
/// Per-cell adaptive iteration calibration. Runs a 100-iter measurement after warmup and computes
|
||||||
/// how many iterations are needed to reach <see cref="TargetSampleMs"/> wall-clock per sample.
|
/// how many iterations are needed to reach <see cref="Configuration.TargetSampleMs"/> wall-clock per sample.
|
||||||
/// Returns iter rounded UP to the nearest 1000, floored at 1000 (the prior fixed minimum) and
|
/// Returns iter rounded UP to the nearest 1000, floored at 1000 (the prior fixed minimum) and
|
||||||
/// ceiling-capped at 200_000 (sanity bound for pathologically fast ops). In Debug single-sample mode
|
/// ceiling-capped at 200_000 (sanity bound for pathologically fast ops). In Debug single-sample mode
|
||||||
/// (<c>BenchmarkSamples <= 1</c>) returns the global <see cref="TestIterations"/> unchanged —
|
/// (<c>Configuration.BenchmarkSamples <= 1</c>) returns the global <see cref="Configuration.TestIterations"/> unchanged —
|
||||||
/// calibration overhead is unjustified there. Calibration runs OUTSIDE the timed sample loop and
|
/// calibration overhead is unjustified there. Calibration runs OUTSIDE the timed sample loop and
|
||||||
/// does NOT count toward warmup; its sole purpose is to measure per-op cost.
|
/// does NOT count toward warmup; its sole purpose is to measure per-op cost.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static int CalibrateIterations(Action action, int targetMs)
|
private static int CalibrateIterations(Action action, int targetMs)
|
||||||
{
|
{
|
||||||
if (BenchmarkSamples <= 1) return TestIterations; // Debug fast path
|
if (Configuration.BenchmarkSamples <= 1) return Configuration.TestIterations; // Debug fast path
|
||||||
|
|
||||||
GC.Collect();
|
GC.Collect();
|
||||||
GC.WaitForPendingFinalizers();
|
GC.WaitForPendingFinalizers();
|
||||||
|
|
@ -1244,7 +1067,7 @@ public static class Program
|
||||||
System.Console.WriteLine(" [A] All layers");
|
System.Console.WriteLine(" [A] All layers");
|
||||||
System.Console.WriteLine(" [F] FastestByte — AcBinary FastMode Byte[] vs MemoryPack Byte[] only (tight optimization loop)");
|
System.Console.WriteLine(" [F] FastestByte — AcBinary FastMode Byte[] vs MemoryPack Byte[] only (tight optimization loop)");
|
||||||
System.Console.WriteLine(" [P] AsyncPipe — streaming I/O isolation (only AsyncPipe, all test data)");
|
System.Console.WriteLine(" [P] AsyncPipe — streaming I/O isolation (only AsyncPipe, all test data)");
|
||||||
System.Console.WriteLine($" [S] Settings — Iteration / WireMode (current: {SelectedWireMode})");
|
System.Console.WriteLine($" [S] Settings — Iteration / WireMode (current: {Configuration.SelectedWireMode})");
|
||||||
System.Console.WriteLine(" [Q] Quit");
|
System.Console.WriteLine(" [Q] Quit");
|
||||||
System.Console.Write("\nSelection: ");
|
System.Console.Write("\nSelection: ");
|
||||||
|
|
||||||
|
|
@ -1270,7 +1093,7 @@ public static class Program
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Settings sub-menu — prompts for Warmup / Iterations / Samples values. Empty input keeps the
|
/// Settings sub-menu — prompts for Warmup / Iterations / Samples values. Empty input keeps the
|
||||||
/// current value. Validation: WarmupIterations ≥ 0; TestIterations ≥ 1; BenchmarkSamples ≥ 1.
|
/// current value. Validation: Configuration.WarmupIterations ≥ 0; Configuration.TestIterations ≥ 1; Configuration.BenchmarkSamples ≥ 1.
|
||||||
/// Returns to the caller (which re-displays the main menu).
|
/// Returns to the caller (which re-displays the main menu).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void ShowSettingsMenu()
|
private static void ShowSettingsMenu()
|
||||||
|
|
@ -1282,7 +1105,7 @@ public static class Program
|
||||||
System.Console.WriteLine("Settings");
|
System.Console.WriteLine("Settings");
|
||||||
System.Console.WriteLine("─────────────────────────────────────────────");
|
System.Console.WriteLine("─────────────────────────────────────────────");
|
||||||
System.Console.WriteLine(" [1] Iteration — Warmup / Iterations / Samples");
|
System.Console.WriteLine(" [1] Iteration — Warmup / Iterations / Samples");
|
||||||
System.Console.WriteLine($" [2] WireMode — current: {SelectedWireMode}");
|
System.Console.WriteLine($" [2] WireMode — current: {Configuration.SelectedWireMode}");
|
||||||
System.Console.WriteLine($" [3] Charset — current: {GetCurrentCharsetName()}");
|
System.Console.WriteLine($" [3] Charset — current: {GetCurrentCharsetName()}");
|
||||||
System.Console.WriteLine(" [B] Back");
|
System.Console.WriteLine(" [B] Back");
|
||||||
System.Console.Write("\nSelection: ");
|
System.Console.Write("\nSelection: ");
|
||||||
|
|
@ -1395,12 +1218,12 @@ public static class Program
|
||||||
System.Console.WriteLine("─────────────────────────────────────────────");
|
System.Console.WriteLine("─────────────────────────────────────────────");
|
||||||
System.Console.WriteLine();
|
System.Console.WriteLine();
|
||||||
|
|
||||||
WarmupIterations = PromptInt("WarmupIterations", WarmupIterations, min: 0);
|
Configuration.WarmupIterations = PromptInt("Configuration.WarmupIterations", Configuration.WarmupIterations, min: 0);
|
||||||
TestIterations = PromptInt("TestIterations ", TestIterations, min: 1);
|
Configuration.TestIterations = PromptInt("Configuration.TestIterations ", Configuration.TestIterations, min: 1);
|
||||||
BenchmarkSamples = PromptInt("BenchmarkSamples", BenchmarkSamples, min: 1);
|
Configuration.BenchmarkSamples = PromptInt("Configuration.BenchmarkSamples", Configuration.BenchmarkSamples, min: 1);
|
||||||
|
|
||||||
System.Console.WriteLine();
|
System.Console.WriteLine();
|
||||||
System.Console.WriteLine($"✓ Iteration settings updated: Warmup={WarmupIterations} | Iterations={TestIterations} | Samples={BenchmarkSamples}");
|
System.Console.WriteLine($"✓ Iteration settings updated: Warmup={Configuration.WarmupIterations} | Iterations={Configuration.TestIterations} | Samples={Configuration.BenchmarkSamples}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ShowWireModeSettingsMenu()
|
private static void ShowWireModeSettingsMenu()
|
||||||
|
|
@ -1411,7 +1234,7 @@ public static class Program
|
||||||
System.Console.WriteLine("─────────────────────────────────────────────");
|
System.Console.WriteLine("─────────────────────────────────────────────");
|
||||||
System.Console.WriteLine("WireMode settings");
|
System.Console.WriteLine("WireMode settings");
|
||||||
System.Console.WriteLine("─────────────────────────────────────────────");
|
System.Console.WriteLine("─────────────────────────────────────────────");
|
||||||
System.Console.WriteLine($"Current: {SelectedWireMode}");
|
System.Console.WriteLine($"Current: {Configuration.SelectedWireMode}");
|
||||||
System.Console.WriteLine(" [1] Compact");
|
System.Console.WriteLine(" [1] Compact");
|
||||||
System.Console.WriteLine(" [2] Fast");
|
System.Console.WriteLine(" [2] Fast");
|
||||||
System.Console.WriteLine(" [B] Back");
|
System.Console.WriteLine(" [B] Back");
|
||||||
|
|
@ -1423,11 +1246,11 @@ public static class Program
|
||||||
switch (char.ToLower(key))
|
switch (char.ToLower(key))
|
||||||
{
|
{
|
||||||
case '1':
|
case '1':
|
||||||
SelectedWireMode = WireMode.Compact;
|
Configuration.SelectedWireMode = WireMode.Compact;
|
||||||
System.Console.WriteLine("✓ WireMode set to Compact");
|
System.Console.WriteLine("✓ WireMode set to Compact");
|
||||||
return;
|
return;
|
||||||
case '2':
|
case '2':
|
||||||
SelectedWireMode = WireMode.Fast;
|
Configuration.SelectedWireMode = WireMode.Fast;
|
||||||
System.Console.WriteLine("✓ WireMode set to Fast");
|
System.Console.WriteLine("✓ WireMode set to Fast");
|
||||||
return;
|
return;
|
||||||
case 'b':
|
case 'b':
|
||||||
|
|
@ -1515,8 +1338,8 @@ public static class Program
|
||||||
/// rankings (because both metrics are misleading there) — they still participate in "Fastest Round-trip".
|
/// rankings (because both metrics are misleading there) — they still participate in "Fastest Round-trip".
|
||||||
/// Default false for in-memory IO modes which measure Ser and Des separately.</summary>
|
/// Default false for in-memory IO modes which measure Ser and Des separately.</summary>
|
||||||
bool IsRoundTripOnly => false;
|
bool IsRoundTripOnly => false;
|
||||||
/// <summary>Combined warmup (Ser + Deser interleaved). Kept for backward-compat with <c>ProfilerMode</c>
|
/// <summary>Combined warmup (Ser + Deser interleaved). Currently unused — kept as a legacy entry point
|
||||||
/// and other callers that don't need phase-separated warmup. The benchmark loop prefers the split
|
/// for any external caller that still wants single-call warmup. The benchmark loop uses the split
|
||||||
/// <see cref="WarmupSerialize"/> + <see cref="WarmupDeserialize"/> pair for cache-isolated measurements.</summary>
|
/// <see cref="WarmupSerialize"/> + <see cref="WarmupDeserialize"/> pair for cache-isolated measurements.</summary>
|
||||||
void Warmup(int iterations);
|
void Warmup(int iterations);
|
||||||
|
|
||||||
|
|
@ -1549,9 +1372,9 @@ public static class Program
|
||||||
private readonly AcBinarySerializerOptions _options;
|
private readonly AcBinarySerializerOptions _options;
|
||||||
private readonly byte[] _serialized;
|
private readonly byte[] _serialized;
|
||||||
|
|
||||||
public string Engine => EngineAcBinary;
|
public string Engine => Configuration.EngineAcBinary;
|
||||||
public string IoMode => IoByteArray;
|
public string IoMode => Configuration.IoByteArray;
|
||||||
public string DispatchMode => _options.UseGeneratedCode ? ModeSGen : ModeRuntime;
|
public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime;
|
||||||
public string OptionsPreset { get; }
|
public string OptionsPreset { get; }
|
||||||
public int SerializedSize => _serialized.Length;
|
public int SerializedSize => _serialized.Length;
|
||||||
public long SetupSerializeAllocBytes => 0;
|
public long SetupSerializeAllocBytes => 0;
|
||||||
|
|
@ -1606,9 +1429,9 @@ public static class Program
|
||||||
private readonly MemoryPackSerializerOptions _options;
|
private readonly MemoryPackSerializerOptions _options;
|
||||||
private readonly byte[] _serialized;
|
private readonly byte[] _serialized;
|
||||||
|
|
||||||
public string Engine => EngineMemoryPack;
|
public string Engine => Configuration.EngineMemoryPack;
|
||||||
public string IoMode => IoByteArray;
|
public string IoMode => Configuration.IoByteArray;
|
||||||
public string DispatchMode => ModeSGen; // MemoryPack always uses [MemoryPackable] source-generated formatters
|
public string DispatchMode => Configuration.ModeSGen; // MemoryPack always uses [MemoryPackable] source-generated formatters
|
||||||
public string OptionsPreset { get; }
|
public string OptionsPreset { get; }
|
||||||
public int SerializedSize => _serialized.Length;
|
public int SerializedSize => _serialized.Length;
|
||||||
public long SetupSerializeAllocBytes => 0;
|
public long SetupSerializeAllocBytes => 0;
|
||||||
|
|
@ -1657,9 +1480,9 @@ public static class Program
|
||||||
private readonly MessagePackSerializerOptions _options;
|
private readonly MessagePackSerializerOptions _options;
|
||||||
private readonly byte[] _serialized;
|
private readonly byte[] _serialized;
|
||||||
|
|
||||||
public string Engine => EngineMessagePack;
|
public string Engine => Configuration.EngineMessagePack;
|
||||||
public string IoMode => IoByteArray;
|
public string IoMode => Configuration.IoByteArray;
|
||||||
public string DispatchMode => ModeSGen; // MessagePack uses [MessagePackObject] source-generated formatters (StandardResolver)
|
public string DispatchMode => Configuration.ModeSGen; // MessagePack uses [MessagePackObject] source-generated formatters (StandardResolver)
|
||||||
public string OptionsPreset { get; }
|
public string OptionsPreset { get; }
|
||||||
public int SerializedSize => _serialized.Length;
|
public int SerializedSize => _serialized.Length;
|
||||||
public long SetupSerializeAllocBytes => 0;
|
public long SetupSerializeAllocBytes => 0;
|
||||||
|
|
@ -1722,9 +1545,9 @@ public static class Program
|
||||||
private readonly AcBinarySerializerOptions _options;
|
private readonly AcBinarySerializerOptions _options;
|
||||||
private readonly byte[] _serialized;
|
private readonly byte[] _serialized;
|
||||||
|
|
||||||
public string Engine => EngineAcBinary;
|
public string Engine => Configuration.EngineAcBinary;
|
||||||
public string IoMode => IoBufWrNew;
|
public string IoMode => Configuration.IoBufWrNew;
|
||||||
public string DispatchMode => _options.UseGeneratedCode ? ModeSGen : ModeRuntime;
|
public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime;
|
||||||
public string OptionsPreset { get; }
|
public string OptionsPreset { get; }
|
||||||
public int SerializedSize => _serialized.Length;
|
public int SerializedSize => _serialized.Length;
|
||||||
public long SetupSerializeAllocBytes => 0;
|
public long SetupSerializeAllocBytes => 0;
|
||||||
|
|
@ -1830,9 +1653,9 @@ public static class Program
|
||||||
private bool _captureResult; // toggle: when true, ConsumeLoop stores result; otherwise discards
|
private bool _captureResult; // toggle: when true, ConsumeLoop stores result; otherwise discards
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
public string Engine => EngineAcBinary;
|
public string Engine => Configuration.EngineAcBinary;
|
||||||
public string IoMode => IoNamedPipe;
|
public string IoMode => Configuration.IoNamedPipe;
|
||||||
public string DispatchMode => _options.UseGeneratedCode ? ModeSGen : ModeRuntime;
|
public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime;
|
||||||
public string OptionsPreset { get; }
|
public string OptionsPreset { get; }
|
||||||
public int SerializedSize => _serialized.Length;
|
public int SerializedSize => _serialized.Length;
|
||||||
public long SetupSerializeAllocBytes { get; }
|
public long SetupSerializeAllocBytes { get; }
|
||||||
|
|
@ -2053,9 +1876,9 @@ public static class Program
|
||||||
private bool _captureResult;
|
private bool _captureResult;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
public string Engine => EngineAcBinary;
|
public string Engine => Configuration.EngineAcBinary;
|
||||||
public string IoMode => IoInMemoryPipe;
|
public string IoMode => Configuration.IoInMemoryPipe;
|
||||||
public string DispatchMode => _options.UseGeneratedCode ? ModeSGen : ModeRuntime;
|
public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime;
|
||||||
public string OptionsPreset { get; }
|
public string OptionsPreset { get; }
|
||||||
public int SerializedSize => _serialized.Length;
|
public int SerializedSize => _serialized.Length;
|
||||||
public long SetupSerializeAllocBytes { get; }
|
public long SetupSerializeAllocBytes { get; }
|
||||||
|
|
@ -2244,9 +2067,9 @@ public static class Program
|
||||||
private bool _captureResult; // toggle: when true, ConsumerLoop stores result; otherwise discards
|
private bool _captureResult; // toggle: when true, ConsumerLoop stores result; otherwise discards
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
public string Engine => EngineAcBinary;
|
public string Engine => Configuration.EngineAcBinary;
|
||||||
public string IoMode => IoNamedPipeRaw;
|
public string IoMode => Configuration.IoNamedPipeRaw;
|
||||||
public string DispatchMode => _options.UseGeneratedCode ? ModeSGen : ModeRuntime;
|
public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime;
|
||||||
public string OptionsPreset { get; }
|
public string OptionsPreset { get; }
|
||||||
public int SerializedSize => _serialized.Length;
|
public int SerializedSize => _serialized.Length;
|
||||||
public long SetupSerializeAllocBytes { get; }
|
public long SetupSerializeAllocBytes { get; }
|
||||||
|
|
@ -2449,9 +2272,9 @@ public static class Program
|
||||||
private bool _captureResult;
|
private bool _captureResult;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
public string Engine => EngineAcBinary;
|
public string Engine => Configuration.EngineAcBinary;
|
||||||
public string IoMode => IoInMemoryRaw;
|
public string IoMode => Configuration.IoInMemoryRaw;
|
||||||
public string DispatchMode => _options.UseGeneratedCode ? ModeSGen : ModeRuntime;
|
public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime;
|
||||||
public string OptionsPreset { get; }
|
public string OptionsPreset { get; }
|
||||||
public int SerializedSize => _serialized.Length;
|
public int SerializedSize => _serialized.Length;
|
||||||
public long SetupSerializeAllocBytes { get; }
|
public long SetupSerializeAllocBytes { get; }
|
||||||
|
|
@ -2593,9 +2416,9 @@ public static class Program
|
||||||
private readonly MemoryPackSerializerOptions _options;
|
private readonly MemoryPackSerializerOptions _options;
|
||||||
private readonly byte[] _serialized;
|
private readonly byte[] _serialized;
|
||||||
|
|
||||||
public string Engine => EngineMemoryPack;
|
public string Engine => Configuration.EngineMemoryPack;
|
||||||
public string IoMode => IoBufWrNew;
|
public string IoMode => Configuration.IoBufWrNew;
|
||||||
public string DispatchMode => ModeSGen; // MemoryPack always uses [MemoryPackable] source-generated formatters
|
public string DispatchMode => Configuration.ModeSGen; // MemoryPack always uses [MemoryPackable] source-generated formatters
|
||||||
public string OptionsPreset { get; }
|
public string OptionsPreset { get; }
|
||||||
public int SerializedSize => _serialized.Length;
|
public int SerializedSize => _serialized.Length;
|
||||||
public long SetupSerializeAllocBytes => 0;
|
public long SetupSerializeAllocBytes => 0;
|
||||||
|
|
@ -2647,9 +2470,9 @@ public static class Program
|
||||||
private readonly byte[] _serialized;
|
private readonly byte[] _serialized;
|
||||||
private readonly ArrayBufferWriter<byte> _bufferWriter;
|
private readonly ArrayBufferWriter<byte> _bufferWriter;
|
||||||
|
|
||||||
public string Engine => EngineAcBinary;
|
public string Engine => Configuration.EngineAcBinary;
|
||||||
public string IoMode => IoBufWrReuse;
|
public string IoMode => Configuration.IoBufWrReuse;
|
||||||
public string DispatchMode => _options.UseGeneratedCode ? ModeSGen : ModeRuntime;
|
public string DispatchMode => _options.UseGeneratedCode ? Configuration.ModeSGen : Configuration.ModeRuntime;
|
||||||
public string OptionsPreset { get; }
|
public string OptionsPreset { get; }
|
||||||
public int SerializedSize => _serialized.Length;
|
public int SerializedSize => _serialized.Length;
|
||||||
public long SetupSerializeAllocBytes { get; }
|
public long SetupSerializeAllocBytes { get; }
|
||||||
|
|
@ -2718,9 +2541,9 @@ public static class Program
|
||||||
private readonly byte[] _serialized;
|
private readonly byte[] _serialized;
|
||||||
private readonly ArrayBufferWriter<byte> _bufferWriter;
|
private readonly ArrayBufferWriter<byte> _bufferWriter;
|
||||||
|
|
||||||
public string Engine => EngineMemoryPack;
|
public string Engine => Configuration.EngineMemoryPack;
|
||||||
public string IoMode => IoBufWrReuse;
|
public string IoMode => Configuration.IoBufWrReuse;
|
||||||
public string DispatchMode => ModeSGen; // MemoryPack always uses [MemoryPackable] source-generated formatters
|
public string DispatchMode => Configuration.ModeSGen; // MemoryPack always uses [MemoryPackable] source-generated formatters
|
||||||
public string OptionsPreset { get; }
|
public string OptionsPreset { get; }
|
||||||
public int SerializedSize => _serialized.Length;
|
public int SerializedSize => _serialized.Length;
|
||||||
public long SetupSerializeAllocBytes { get; }
|
public long SetupSerializeAllocBytes { get; }
|
||||||
|
|
@ -2779,9 +2602,9 @@ public static class Program
|
||||||
private readonly string _serialized;
|
private readonly string _serialized;
|
||||||
private readonly byte[] _serializedUtf8;
|
private readonly byte[] _serializedUtf8;
|
||||||
|
|
||||||
public string Engine => EngineSystemTextJson;
|
public string Engine => Configuration.EngineSystemTextJson;
|
||||||
public string IoMode => IoString;
|
public string IoMode => Configuration.IoString;
|
||||||
public string DispatchMode => ModeRuntime; // System.Text.Json default uses reflection-based metadata (no source generator opt-in here)
|
public string DispatchMode => Configuration.ModeRuntime; // System.Text.Json default uses reflection-based metadata (no source generator opt-in here)
|
||||||
public string OptionsPreset { get; }
|
public string OptionsPreset { get; }
|
||||||
public int SerializedSize => _serializedUtf8.Length;
|
public int SerializedSize => _serializedUtf8.Length;
|
||||||
public long SetupSerializeAllocBytes => 0;
|
public long SetupSerializeAllocBytes => 0;
|
||||||
|
|
@ -2798,7 +2621,7 @@ public static class Program
|
||||||
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 = Utf8NoBom.GetBytes(_serialized);
|
_serializedUtf8 = Configuration.Utf8NoBom.GetBytes(_serialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Warmup(int iterations)
|
public void Warmup(int iterations)
|
||||||
|
|
@ -2854,11 +2677,11 @@ public static class Program
|
||||||
public double DeserializeTimeMinMs { get; set; }
|
public double DeserializeTimeMinMs { get; set; }
|
||||||
public double DeserializeTimeMaxMs { get; set; }
|
public double DeserializeTimeMaxMs { get; set; }
|
||||||
// Sample-population stddev (ms). Used by FormatMicrosWithRange to compute CV (stddev/mean)
|
// Sample-population stddev (ms). Used by FormatMicrosWithRange to compute CV (stddev/mean)
|
||||||
// and emit the ⚠️ marker on rows above UnstableCVThreshold. 0 in single-sample mode.
|
// and emit the ⚠️ marker on rows above Configuration.UnstableCVThreshold. 0 in single-sample mode.
|
||||||
public double SerializeTimeStdDevMs { get; set; }
|
public double SerializeTimeStdDevMs { get; set; }
|
||||||
public double DeserializeTimeStdDevMs { get; set; }
|
public double DeserializeTimeStdDevMs { get; set; }
|
||||||
// Per-row adaptive iteration count (post-CalibrateIterations). Each Ser and Des function calibrates
|
// Per-row adaptive iteration count (post-CalibrateIterations). Each Ser and Des function calibrates
|
||||||
// independently to land its sample window at ~TargetSampleMs; per-op µs is then iter-independent
|
// independently to land its sample window at ~Configuration.TargetSampleMs; per-op µs is then iter-independent
|
||||||
// (`SerializeTimeMs / SerializeIterations * 1000`). For round-trip-only rows (NamedPipe etc.),
|
// (`SerializeTimeMs / SerializeIterations * 1000`). For round-trip-only rows (NamedPipe etc.),
|
||||||
// RoundTripIterations carries the calibrated iter count; SerializeIterations and DeserializeIterations
|
// RoundTripIterations carries the calibrated iter count; SerializeIterations and DeserializeIterations
|
||||||
// stay 0 (Ser and Des are not separately measurable on those rows).
|
// stay 0 (Ser and Des are not separately measurable on those rows).
|
||||||
|
|
@ -2923,10 +2746,10 @@ public static class Program
|
||||||
// Order by per-op µs (iter-independent) — rows may have different iter counts post-calibration.
|
// Order by per-op µs (iter-independent) — rows may have different iter counts post-calibration.
|
||||||
var testResults = results.Where(r => r.TestDataName == testData.DisplayName).OrderBy(r => RtPerOp(r)).ToList();
|
var testResults = results.Where(r => r.TestDataName == testData.DisplayName).OrderBy(r => RtPerOp(r)).ToList();
|
||||||
// Baseline switched MessagePack → MemoryPack: MemoryPack is the SOTA performance leader.
|
// Baseline switched MessagePack → MemoryPack: MemoryPack is the SOTA performance leader.
|
||||||
var memPackResult = testResults.FirstOrDefault(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray));
|
var memPackResult = testResults.FirstOrDefault(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray));
|
||||||
// Pin the comparison to AcBinary's SGen variant — apples-to-apples vs MemoryPack (also source-generated).
|
// Pin the comparison to AcBinary's SGen variant — apples-to-apples vs MemoryPack (also source-generated).
|
||||||
// The Runtime variant is shown alongside in the table for context, not used as the headline number.
|
// The Runtime variant is shown alongside in the table for context, not used as the headline number.
|
||||||
var acBinaryResult = testResults.FirstOrDefault(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen));
|
var acBinaryResult = testResults.FirstOrDefault(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen));
|
||||||
|
|
||||||
System.Console.WriteLine($"\n┌─ {testData.DisplayName} ─".PadRight(172, '─') + "┐");
|
System.Console.WriteLine($"\n┌─ {testData.DisplayName} ─".PadRight(172, '─') + "┐");
|
||||||
// Header-only units; per-row entries are numbers (µs/op for time, KB/op for alloc, KB pair "ser / des" for Setup, B for Size).
|
// Header-only units; per-row entries are numbers (µs/op for time, KB/op for alloc, KB pair "ser / des" for Setup, B for Size).
|
||||||
|
|
@ -2947,8 +2770,8 @@ public static class Program
|
||||||
|
|
||||||
// Highlight MemoryPack baseline (any Byte[]) and AcBinary headline contender (Byte[] + SGen) with win/lose colors.
|
// Highlight MemoryPack baseline (any Byte[]) and AcBinary headline contender (Byte[] + SGen) with win/lose colors.
|
||||||
// The AcBinary Byte[]+Runtime variant is shown unhighlighted — it's contextual (SGen speed-up reference), not the headline.
|
// The AcBinary Byte[]+Runtime variant is shown unhighlighted — it's contextual (SGen speed-up reference), not the headline.
|
||||||
var isHighlighted = (result.Engine == EngineMemoryPack && result.IoMode == IoByteArray)
|
var isHighlighted = (result.Engine == Configuration.EngineMemoryPack && result.IoMode == Configuration.IoByteArray)
|
||||||
|| (result.Engine == EngineAcBinary && result.IoMode == IoByteArray && result.DispatchMode == ModeSGen);
|
|| (result.Engine == Configuration.EngineAcBinary && result.IoMode == Configuration.IoByteArray && result.DispatchMode == Configuration.ModeSGen);
|
||||||
|
|
||||||
var prefix = isHighlighted ? "│►" : "│ ";
|
var prefix = isHighlighted ? "│►" : "│ ";
|
||||||
var suffix = isHighlighted ? "◄│" : " │";
|
var suffix = isHighlighted ? "◄│" : " │";
|
||||||
|
|
@ -2956,7 +2779,7 @@ public static class Program
|
||||||
// Color logic: Green = winner (faster), Red = loser (slower)
|
// Color logic: Green = winner (faster), Red = loser (slower)
|
||||||
if (isHighlighted && memPackResult != null && acBinaryResult != null)
|
if (isHighlighted && memPackResult != null && acBinaryResult != null)
|
||||||
{
|
{
|
||||||
var isMemPack = (result.Engine == EngineMemoryPack && result.IoMode == IoByteArray);
|
var isMemPack = (result.Engine == Configuration.EngineMemoryPack && result.IoMode == Configuration.IoByteArray);
|
||||||
var memPackFaster = RtPerOp(memPackResult) < RtPerOp(acBinaryResult);
|
var memPackFaster = RtPerOp(memPackResult) < RtPerOp(acBinaryResult);
|
||||||
|
|
||||||
if (isMemPack)
|
if (isMemPack)
|
||||||
|
|
@ -3103,13 +2926,13 @@ public static class Program
|
||||||
// Overall AcBinary (SGen) vs MemoryPack comparison (baseline switched MessagePack → MemoryPack as SOTA reference).
|
// Overall AcBinary (SGen) vs MemoryPack comparison (baseline switched MessagePack → MemoryPack as SOTA reference).
|
||||||
// AcBinary side is restricted to DispatchMode == SGen — apples-to-apples vs MemoryPack which is also source-generated.
|
// AcBinary side is restricted to DispatchMode == SGen — apples-to-apples vs MemoryPack which is also source-generated.
|
||||||
// The Runtime variant is shown side-by-side in each per-test fancy table for SGen-speedup context, but excluded from this headline.
|
// The Runtime variant is shown side-by-side in each per-test fancy table for SGen-speedup context, but excluded from this headline.
|
||||||
var memPackSerResults = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray) && r.SerializeTimeMs > 0).ToList();
|
var memPackSerResults = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray) && r.SerializeTimeMs > 0).ToList();
|
||||||
var memPackDesResults = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray) && r.DeserializeTimeMs > 0).ToList();
|
var memPackDesResults = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray) && r.DeserializeTimeMs > 0).ToList();
|
||||||
var memPackRtResults = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray) && r.RoundTripTimeMs > 0).ToList();
|
var memPackRtResults = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray) && r.RoundTripTimeMs > 0).ToList();
|
||||||
|
|
||||||
var acBinarySerResults = results.Where(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen) && r.SerializeTimeMs > 0).ToList();
|
var acBinarySerResults = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen) && r.SerializeTimeMs > 0).ToList();
|
||||||
var acBinaryDesResults = results.Where(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen) && r.DeserializeTimeMs > 0).ToList();
|
var acBinaryDesResults = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen) && r.DeserializeTimeMs > 0).ToList();
|
||||||
var acBinaryRtResults = results.Where(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen) && r.RoundTripTimeMs > 0).ToList();
|
var acBinaryRtResults = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen) && r.RoundTripTimeMs > 0).ToList();
|
||||||
|
|
||||||
// Skip comparison if no data available
|
// Skip comparison if no data available
|
||||||
if (memPackRtResults.Count == 0 || acBinaryRtResults.Count == 0)
|
if (memPackRtResults.Count == 0 || acBinaryRtResults.Count == 0)
|
||||||
|
|
@ -3128,8 +2951,8 @@ public static class Program
|
||||||
// - Median of per-cell ratios — outlier-resistant.
|
// - Median of per-cell ratios — outlier-resistant.
|
||||||
// The geo/median variants surface when a single cell dominates the arithmetic average
|
// The geo/median variants surface when a single cell dominates the arithmetic average
|
||||||
// (typical when one cell's µs-per-op is an order of magnitude larger than the others).
|
// (typical when one cell's µs-per-op is an order of magnitude larger than the others).
|
||||||
var sizeAcResults = results.Where(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen)).ToList();
|
var sizeAcResults = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen)).ToList();
|
||||||
var sizeMpResults = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray)).ToList();
|
var sizeMpResults = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray)).ToList();
|
||||||
|
|
||||||
var serStats = ComputeOverallStats(acBinarySerResults, memPackSerResults, SerPerOp);
|
var serStats = ComputeOverallStats(acBinarySerResults, memPackSerResults, SerPerOp);
|
||||||
var desStats = ComputeOverallStats(acBinaryDesResults, memPackDesResults, DesPerOp);
|
var desStats = ComputeOverallStats(acBinaryDesResults, memPackDesResults, DesPerOp);
|
||||||
|
|
@ -3183,12 +3006,12 @@ public static class Program
|
||||||
|
|
||||||
private static void SaveResults(List<BenchmarkResult> results, List<TestDataSet> testDataSets)
|
private static void SaveResults(List<BenchmarkResult> results, List<TestDataSet> testDataSets)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(ResultsDirectory);
|
Directory.CreateDirectory(Configuration.ResultsDirectory);
|
||||||
|
|
||||||
var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
|
var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
|
||||||
var baseFileName = $"Console.FullBenchmark_{BuildConfiguration}_{timestamp}";
|
var baseFileName = $"Console.FullBenchmark_{Configuration.BuildConfiguration}_{timestamp}";
|
||||||
var logFilePath = Path.Combine(ResultsDirectory, $"{baseFileName}.log");
|
var logFilePath = Path.Combine(Configuration.ResultsDirectory, $"{baseFileName}.log");
|
||||||
var outputFilePath = Path.Combine(ResultsDirectory, $"{baseFileName}.output");
|
var outputFilePath = Path.Combine(Configuration.ResultsDirectory, $"{baseFileName}.output");
|
||||||
|
|
||||||
// Save binary output to separate .output file
|
// Save binary output to separate .output file
|
||||||
var largeTestData = testDataSets.FirstOrDefault(t => t.Name.StartsWith("Large"));
|
var largeTestData = testDataSets.FirstOrDefault(t => t.Name.StartsWith("Large"));
|
||||||
|
|
@ -3208,7 +3031,7 @@ public static class Program
|
||||||
outputSb.AppendLine("Hex dump:");
|
outputSb.AppendLine("Hex dump:");
|
||||||
outputSb.AppendLine(FormatHexDump(serializedBytes));
|
outputSb.AppendLine(FormatHexDump(serializedBytes));
|
||||||
|
|
||||||
File.WriteAllText(outputFilePath, outputSb.ToString(), Utf8NoBom);
|
File.WriteAllText(outputFilePath, outputSb.ToString(), Configuration.Utf8NoBom);
|
||||||
System.Console.WriteLine($"✓ Binary output saved to: {outputFilePath}");
|
System.Console.WriteLine($"✓ Binary output saved to: {outputFilePath}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3217,10 +3040,10 @@ public static class Program
|
||||||
sb.AppendLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗");
|
sb.AppendLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗");
|
||||||
sb.AppendLine("║ SERIALIZER BENCHMARK RESULTS ║");
|
sb.AppendLine("║ SERIALIZER BENCHMARK RESULTS ║");
|
||||||
sb.AppendLine($"║ Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}".PadRight(100) + "║");
|
sb.AppendLine($"║ Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}".PadRight(100) + "║");
|
||||||
sb.AppendLine($"║ Build: {BuildConfiguration}".PadRight(100) + "║");
|
sb.AppendLine($"║ Build: {Configuration.BuildConfiguration}".PadRight(100) + "║");
|
||||||
sb.AppendLine($"║ Charset: {GetCurrentCharsetName()}".PadRight(100) + "║");
|
sb.AppendLine($"║ Charset: {GetCurrentCharsetName()}".PadRight(100) + "║");
|
||||||
sb.AppendLine($"║ Iterations: per-cell adaptive (~{TargetSampleMs} ms target)".PadRight(100) + "║");
|
sb.AppendLine($"║ Iterations: per-cell adaptive (~{Configuration.TargetSampleMs} ms target)".PadRight(100) + "║");
|
||||||
sb.AppendLine($"║ Samples: {BenchmarkSamples} (median) + 1 pilot discarded".PadRight(100) + "║");
|
sb.AppendLine($"║ Samples: {Configuration.BenchmarkSamples} (median) + 1 pilot discarded".PadRight(100) + "║");
|
||||||
sb.AppendLine($"║ Test Type: {testDataSets.FirstOrDefault()?.TypeName ?? "unknown"}".PadRight(100) + "║");
|
sb.AppendLine($"║ Test Type: {testDataSets.FirstOrDefault()?.TypeName ?? "unknown"}".PadRight(100) + "║");
|
||||||
sb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
|
sb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
|
|
@ -3262,10 +3085,10 @@ public static class Program
|
||||||
{
|
{
|
||||||
// Order by per-op µs (iter-independent) — rows may have different iter counts post-calibration.
|
// Order by per-op µs (iter-independent) — rows may have different iter counts post-calibration.
|
||||||
var testResults = results.Where(r => r.TestDataName == testData.DisplayName).OrderBy(r => RtPerOp(r)).ToList();
|
var testResults = results.Where(r => r.TestDataName == testData.DisplayName).OrderBy(r => RtPerOp(r)).ToList();
|
||||||
var memPackResult = testResults.FirstOrDefault(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray));
|
var memPackResult = testResults.FirstOrDefault(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray));
|
||||||
// Pin the comparison to AcBinary's SGen variant — apples-to-apples vs MemoryPack (also source-generated).
|
// Pin the comparison to AcBinary's SGen variant — apples-to-apples vs MemoryPack (also source-generated).
|
||||||
// The Runtime variant is shown alongside in the table for context, not used as the headline number.
|
// The Runtime variant is shown alongside in the table for context, not used as the headline number.
|
||||||
var acBinaryResult = testResults.FirstOrDefault(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen));
|
var acBinaryResult = testResults.FirstOrDefault(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen));
|
||||||
|
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
sb.AppendLine($"--- {testData.DisplayName} ---");
|
sb.AppendLine($"--- {testData.DisplayName} ---");
|
||||||
|
|
@ -3275,7 +3098,7 @@ public static class Program
|
||||||
var rank = 1;
|
var rank = 1;
|
||||||
foreach (var result in testResults)
|
foreach (var result in testResults)
|
||||||
{
|
{
|
||||||
var isHighlighted = ((result.Engine == EngineMemoryPack || result.Engine == EngineAcBinary) && result.IoMode == IoByteArray);
|
var isHighlighted = ((result.Engine == Configuration.EngineMemoryPack || result.Engine == Configuration.EngineAcBinary) && result.IoMode == Configuration.IoByteArray);
|
||||||
var prefix = isHighlighted ? "► " : " ";
|
var prefix = isHighlighted ? "► " : " ";
|
||||||
|
|
||||||
var size = $"{result.SerializedSize:N0}";
|
var size = $"{result.SerializedSize:N0}";
|
||||||
|
|
@ -3312,13 +3135,13 @@ public static class Program
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
sb.AppendLine("=== AcBinary (Byte[], SGen) vs MemoryPack (Byte[]) (Overall) ===");
|
sb.AppendLine("=== AcBinary (Byte[], SGen) vs MemoryPack (Byte[]) (Overall) ===");
|
||||||
|
|
||||||
var memPackSerResults2 = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray) && r.SerializeTimeMs > 0).ToList();
|
var memPackSerResults2 = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray) && r.SerializeTimeMs > 0).ToList();
|
||||||
var memPackDesResults2 = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray) && r.DeserializeTimeMs > 0).ToList();
|
var memPackDesResults2 = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray) && r.DeserializeTimeMs > 0).ToList();
|
||||||
var memPackRtResults2 = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray) && r.RoundTripTimeMs > 0).ToList();
|
var memPackRtResults2 = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray) && r.RoundTripTimeMs > 0).ToList();
|
||||||
|
|
||||||
var acBinarySerResults2 = results.Where(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen) && r.SerializeTimeMs > 0).ToList();
|
var acBinarySerResults2 = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen) && r.SerializeTimeMs > 0).ToList();
|
||||||
var acBinaryDesResults2 = results.Where(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen) && r.DeserializeTimeMs > 0).ToList();
|
var acBinaryDesResults2 = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen) && r.DeserializeTimeMs > 0).ToList();
|
||||||
var acBinaryRtResults2 = results.Where(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen) && r.RoundTripTimeMs > 0).ToList();
|
var acBinaryRtResults2 = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen) && r.RoundTripTimeMs > 0).ToList();
|
||||||
|
|
||||||
// Skip comparison block if either side has no Byte[] data — happens in AsyncPipe-only mode
|
// Skip comparison block if either side has no Byte[] data — happens in AsyncPipe-only mode
|
||||||
// where only NamedPipe rows exist (no MemoryPack baseline, no AcBinary Byte[] reference).
|
// where only NamedPipe rows exist (no MemoryPack baseline, no AcBinary Byte[] reference).
|
||||||
|
|
@ -3326,18 +3149,18 @@ public static class Program
|
||||||
if (memPackRtResults2.Count == 0 || acBinaryRtResults2.Count == 0)
|
if (memPackRtResults2.Count == 0 || acBinaryRtResults2.Count == 0)
|
||||||
{
|
{
|
||||||
sb.AppendLine(" (Comparison requires both serialize and deserialize data)");
|
sb.AppendLine(" (Comparison requires both serialize and deserialize data)");
|
||||||
File.WriteAllText(logFilePath, sb.ToString(), Utf8NoBom);
|
File.WriteAllText(logFilePath, sb.ToString(), Configuration.Utf8NoBom);
|
||||||
System.Console.WriteLine($"✓ Results saved to: {logFilePath}");
|
System.Console.WriteLine($"✓ Results saved to: {logFilePath}");
|
||||||
|
|
||||||
var llmFilePathEarly = Path.Combine(ResultsDirectory, $"{baseFileName}.LLM");
|
var llmFilePathEarly = Path.Combine(Configuration.ResultsDirectory, $"{baseFileName}.LLM");
|
||||||
SaveLlmResults(llmFilePathEarly, results, testDataSets);
|
SaveLlmResults(llmFilePathEarly, results, testDataSets);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-cell-paired aggregation: arithmetic / geometric / median. See PrintSummary's parallel
|
// Per-cell-paired aggregation: arithmetic / geometric / median. See PrintSummary's parallel
|
||||||
// block + the OverallStats record for the rationale (per-cell ratio vs magnitude-weighted mean).
|
// block + the OverallStats record for the rationale (per-cell ratio vs magnitude-weighted mean).
|
||||||
var sizeAcResults2 = results.Where(r => (r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen)).ToList();
|
var sizeAcResults2 = results.Where(r => (r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen)).ToList();
|
||||||
var sizeMpResults2 = results.Where(r => (r.Engine == EngineMemoryPack && r.IoMode == IoByteArray)).ToList();
|
var sizeMpResults2 = results.Where(r => (r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray)).ToList();
|
||||||
|
|
||||||
AppendOverallLine(sb, "Serialize", "µs/op", ComputeOverallStats(acBinarySerResults2, memPackSerResults2, SerPerOp));
|
AppendOverallLine(sb, "Serialize", "µs/op", ComputeOverallStats(acBinarySerResults2, memPackSerResults2, SerPerOp));
|
||||||
AppendOverallLine(sb, "Ser Alloc", "B/op", ComputeOverallStats(acBinarySerResults2, memPackSerResults2, r => r.SerializeAllocBytesPerOp), "F0");
|
AppendOverallLine(sb, "Ser Alloc", "B/op", ComputeOverallStats(acBinarySerResults2, memPackSerResults2, r => r.SerializeAllocBytesPerOp), "F0");
|
||||||
|
|
@ -3346,11 +3169,11 @@ public static class Program
|
||||||
AppendOverallLine(sb, "Round-trip", "µs/op", ComputeOverallStats(acBinaryRtResults2, memPackRtResults2, RtPerOp));
|
AppendOverallLine(sb, "Round-trip", "µs/op", ComputeOverallStats(acBinaryRtResults2, memPackRtResults2, RtPerOp));
|
||||||
AppendOverallLine(sb, "Size", "B", ComputeOverallStats(sizeAcResults2, sizeMpResults2, r => r.SerializedSize), "F0");
|
AppendOverallLine(sb, "Size", "B", ComputeOverallStats(sizeAcResults2, sizeMpResults2, r => r.SerializedSize), "F0");
|
||||||
|
|
||||||
File.WriteAllText(logFilePath, sb.ToString(), Utf8NoBom);
|
File.WriteAllText(logFilePath, sb.ToString(), Configuration.Utf8NoBom);
|
||||||
System.Console.WriteLine($"✓ Results saved to: {logFilePath}");
|
System.Console.WriteLine($"✓ Results saved to: {logFilePath}");
|
||||||
|
|
||||||
// Save LLM-optimized results
|
// Save LLM-optimized results
|
||||||
var llmFilePath = Path.Combine(ResultsDirectory, $"{baseFileName}.LLM");
|
var llmFilePath = Path.Combine(Configuration.ResultsDirectory, $"{baseFileName}.LLM");
|
||||||
SaveLlmResults(llmFilePath, results, testDataSets);
|
SaveLlmResults(llmFilePath, results, testDataSets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3358,8 +3181,8 @@ public static class Program
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
var testTypeName = testDataSets.FirstOrDefault()?.TypeName ?? "unknown";
|
var testTypeName = testDataSets.FirstOrDefault()?.TypeName ?? "unknown";
|
||||||
sb.AppendLine($"# AcBinary Benchmark {BuildConfiguration} {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
|
sb.AppendLine($"# AcBinary Benchmark {Configuration.BuildConfiguration} {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
|
||||||
sb.AppendLine($"Charset: {GetCurrentCharsetName()} | Iterations: per-cell adaptive (target ~{TargetSampleMs} ms/sample) | Warmup: {WarmupIterations} per phase (Ser/Des isolated) | Samples: {BenchmarkSamples} (median) + 1 pilot discarded | .NET: {Environment.Version} | TestType: {testTypeName} | UnstableCV threshold: {UnstableCVThreshold * 100:F0}%");
|
sb.AppendLine($"Charset: {GetCurrentCharsetName()} | Iterations: per-cell adaptive (target ~{Configuration.TargetSampleMs} ms/sample) | Warmup: {Configuration.WarmupIterations} per phase (Ser/Des isolated) | Samples: {Configuration.BenchmarkSamples} (median) + 1 pilot discarded | .NET: {Environment.Version} | TestType: {testTypeName} | UnstableCV threshold: {Configuration.UnstableCVThreshold * 100:F0}%");
|
||||||
sb.AppendLine("Baseline: MemoryPack (Byte[]) (SOTA reference) | Verified: round-trip correctness checked once per cell before warmup");
|
sb.AppendLine("Baseline: MemoryPack (Byte[]) (SOTA reference) | Verified: round-trip correctness checked once per cell before warmup");
|
||||||
|
|
||||||
// Options summary
|
// Options summary
|
||||||
|
|
@ -3379,7 +3202,7 @@ public static class Program
|
||||||
|
|
||||||
// Flat results table sorted by test data then round-trip (now includes Alloc + Iter columns).
|
// Flat results table sorted by test data then round-trip (now includes Alloc + Iter columns).
|
||||||
// Iter column shows per-row Ser/Des iteration counts (post-adaptive-calibration), so the reader
|
// Iter column shows per-row Ser/Des iteration counts (post-adaptive-calibration), so the reader
|
||||||
// can verify that each cell's batch sample landed near the TargetSampleMs window.
|
// can verify that each cell's batch sample landed near the Configuration.TargetSampleMs window.
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
sb.AppendLine("## Results");
|
sb.AppendLine("## Results");
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
|
|
@ -3429,8 +3252,8 @@ public static class Program
|
||||||
// arith mean is magnitude-weighted (Large cell dominates); geo/median are per-cell-equal
|
// arith mean is magnitude-weighted (Large cell dominates); geo/median are per-cell-equal
|
||||||
// signals. Adding this lets an LLM diagnose whether a headline delta is a real overall
|
// signals. Adding this lets an LLM diagnose whether a headline delta is a real overall
|
||||||
// win/loss or a single-cell artifact.
|
// win/loss or a single-cell artifact.
|
||||||
var memPackByteArrayResults = results.Where(r => r.Engine == EngineMemoryPack && r.IoMode == IoByteArray).ToList();
|
var memPackByteArrayResults = results.Where(r => r.Engine == Configuration.EngineMemoryPack && r.IoMode == Configuration.IoByteArray).ToList();
|
||||||
var acBinarySGenByteArrayResults = results.Where(r => r.Engine == EngineAcBinary && r.IoMode == IoByteArray && r.DispatchMode == ModeSGen).ToList();
|
var acBinarySGenByteArrayResults = results.Where(r => r.Engine == Configuration.EngineAcBinary && r.IoMode == Configuration.IoByteArray && r.DispatchMode == Configuration.ModeSGen).ToList();
|
||||||
var memPackSerResultsLlm = memPackByteArrayResults.Where(r => r.SerializeTimeMs > 0).ToList();
|
var memPackSerResultsLlm = memPackByteArrayResults.Where(r => r.SerializeTimeMs > 0).ToList();
|
||||||
var memPackDesResultsLlm = memPackByteArrayResults.Where(r => r.DeserializeTimeMs > 0).ToList();
|
var memPackDesResultsLlm = memPackByteArrayResults.Where(r => r.DeserializeTimeMs > 0).ToList();
|
||||||
var memPackRtResultsLlm = memPackByteArrayResults.Where(r => r.RoundTripTimeMs > 0).ToList();
|
var memPackRtResultsLlm = memPackByteArrayResults.Where(r => r.RoundTripTimeMs > 0).ToList();
|
||||||
|
|
@ -3455,7 +3278,7 @@ public static class Program
|
||||||
sb.AppendLine("```");
|
sb.AppendLine("```");
|
||||||
}
|
}
|
||||||
|
|
||||||
File.WriteAllText(filePath, sb.ToString(), Utf8NoBom);
|
File.WriteAllText(filePath, sb.ToString(), Configuration.Utf8NoBom);
|
||||||
System.Console.WriteLine($"✓ LLM results saved to: {filePath}");
|
System.Console.WriteLine($"✓ LLM results saved to: {filePath}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ Standalone benchmark console application for comparing serializer performance. T
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
- **`Program.cs`** — Benchmark runner. Modes: `all` (default), `quick` (fewer iterations), `serialize`, `deserialize`, `profiler` (memory profiler warmup). Outputs results to `Test_Benchmark_Results/Benchmark/`. Iterations: 5000 warmup + 1000 test (Release), 0+1 (Debug).
|
- **`Program.cs`** — Benchmark runner. Modes: `all` (default), `quick` (fewer iterations), `serialize`, `deserialize`. Outputs results to `Test_Benchmark_Results/Benchmark/`. Iterations: 5000 warmup + 1000 test (Release), 0+1 (Debug).
|
||||||
- **`BenchmarkTestDataProvider.cs`** — Test data factory producing 5 data shapes:
|
- **`BenchmarkTestDataProvider.cs`** — Test data factory producing 5 data shapes:
|
||||||
- Small (2x2x2x2), Medium (3x3x3x4), Large (5x5x5x10)
|
- Small (2x2x2x2), Medium (3x3x3x4), Large (5x5x5x10)
|
||||||
- Repeated Strings (10 items, string deduplication testing)
|
- Repeated Strings (10 items, string deduplication testing)
|
||||||
|
|
|
||||||
|
|
@ -78,20 +78,6 @@ public static class BenchmarkTestDataProvider
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TestOrder CreateProfilerOrder()
|
|
||||||
{
|
|
||||||
TestDataFactory.ResetIdCounter();
|
|
||||||
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
|
||||||
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
|
||||||
return TestDataFactory.CreateOrder(
|
|
||||||
itemCount: 3,
|
|
||||||
palletsPerItem: 3,
|
|
||||||
measurementsPerPallet: 3,
|
|
||||||
pointsPerMeasurement: 4,
|
|
||||||
sharedTag: sharedTag,
|
|
||||||
sharedUser: sharedUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TestDataSet CreateSmallTestData(bool resetId = true)
|
private static TestDataSet CreateSmallTestData(bool resetId = true)
|
||||||
{
|
{
|
||||||
if (resetId) TestDataFactory.ResetIdCounter();
|
if (resetId) TestDataFactory.ResetIdCounter();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue