using AyCode.Core.Benchmarks.Reporting;
using AyCode.Core.Benchmarks.Workloads.Scenarios;
using AyCode.Core.Serializers;
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Tests.TestModels;
using BenchmarkDotNet.Reports;
using System.Text;
namespace AyCode.Core.Benchmarks;
///
/// Translates (BDN's post-run aggregate) into the unified
/// rows consumed by , then emits the
/// Bdn.FullBenchmark_*.{log,LLM,output} triplet alongside Console's counterparts in
/// 's Test_Benchmark_Results/Benchmark/.
///
/// Why a separate adapter: BDN's Summary is per-method (Serialize / Deserialize as separate
/// s, parameterised by TestData + Engine). The unified format collapses these
/// into per-cell rows (one row per TestData × Engine with both Ser and Des stats inline). The adapter
/// groups, transposes, and converts ns → ms before handing off to the shared writer.
///
/// Mean vs Median: maps BDN's into
/// the BenchmarkResult's time columns — same convention as Console (which captures sample-median).
/// Min/Max/StdDev populate the inter-sample range surfaced in
/// (incl. CV-warning ⚠️ marker when stddev/median exceeds ).
///
/// Iteration count = 1: BDN reports per-operation time (ns) — already amortized across N
/// invocations. The unified BenchmarkResult expects total-batch time + iteration count (so µs/op =
/// timeMs / iterations * 1000). Storing Mean-in-ms with iterations = 1 makes the same formula yield
/// Mean-in-µs directly. The actual BDN N count is recorded in the BDN-native artifacts (.../BDN/...)
/// for anyone who wants the raw invocation count.
///
public static class BdnSummaryAdapter
{
///
/// Post-run entry point — call once after BenchmarkRunner.Run<AcBinaryVsMemPackBenchmark>(...)
/// returns. Produces the BDN-side Bdn.* file triplet AND prints the grouped-results console table
/// (same view Console produces post-run) so the user sees the cell-level deltas immediately, without
/// having to open the .LLM file.
///
public static void WriteResults(Summary summary)
{
// Parent-process counterpart of AcBinaryVsMemPackBenchmark.Setup's charset pin: the BDN child
// processes ran Latin1Short, but this adapter runs in the parent process where LongStringSuffix
// would still be the compile-time default (Latin1Long). Set it so GetCharsetName() labels the
// .LLM correctly AND the CreateTestDataSets()/CreateWorkload calls below compute matching Size(B).
BenchmarkTestDataProvider.LongStringSuffix = CharsetSuffixes.Latin1Short;
var allTestData = BenchmarkTestDataProvider_All_False.CreateTestDataSets();
var results = Translate(summary, allTestData);
var ctx = CreateContext();
BenchmarkReportWriter.PrintGroupedResults(results, allTestData);
BenchmarkReportWriter.SaveAll(ctx, results, allTestData);
}
private static ReportingContext CreateContext()
{
#if DEBUG
const string buildConfig = "Debug";
#elif SGEN_ONLY
const string buildConfig = "SGenOnly";
#else
const string buildConfig = "Release";
#endif
return new ReportingContext(
SourceTag: "Bdn",
ResultsDirectory: ReportingContext.ResolveResultsDirectory(),
BuildConfiguration: buildConfig,
Utf8NoBom: new UTF8Encoding(encoderShouldEmitUTF8Identifier: false),
CharsetName: GetCharsetName(),
// Warmup / Samples / TargetSampleMs are BDN-managed (not Console's adaptive engine). Zeros here
// signal "BDN handled internally" in the header; the BDN-native artifacts under .../BDN/ have
// the exact BDN config (warmup count, iteration count, run strategy) for anyone who needs it.
WarmupIterations: 0,
BenchmarkSamples: 0,
TargetSampleMs: 0,
UnstableCVThreshold: 0.03);
}
///
/// Looks up the human-readable name for the currently-active
/// charset. Mirrors Console's Configuration.GetCurrentCharsetName. The BDN serializer benchmark
/// pins the charset to Latin1Short — set in (parent process) and in
/// AcBinaryVsMemPackBenchmark.Setup (child process); see those sites for the process-isolation
/// rationale (BDN's per-benchmark child processes don't inherit a parent static-field mutation).
///
private static string GetCharsetName()
{
var s = BenchmarkTestDataProvider.LongStringSuffix;
return s switch
{
CharsetSuffixes.AsciiFix => nameof(CharsetSuffixes.AsciiFix),
CharsetSuffixes.AsciiShort => nameof(CharsetSuffixes.AsciiShort),
CharsetSuffixes.AsciiLong => nameof(CharsetSuffixes.AsciiLong),
CharsetSuffixes.Latin1Fix => nameof(CharsetSuffixes.Latin1Fix),
CharsetSuffixes.Latin1Short => nameof(CharsetSuffixes.Latin1Short),
CharsetSuffixes.Latin1Long => nameof(CharsetSuffixes.Latin1Long),
CharsetSuffixes.CjkBmpShort => nameof(CharsetSuffixes.CjkBmpShort),
CharsetSuffixes.CjkBmpLong => nameof(CharsetSuffixes.CjkBmpLong),
CharsetSuffixes.CyrillicShort => nameof(CharsetSuffixes.CyrillicShort),
CharsetSuffixes.CyrillicLong => nameof(CharsetSuffixes.CyrillicLong),
CharsetSuffixes.MixedShort => nameof(CharsetSuffixes.MixedShort),
CharsetSuffixes.MixedLong => nameof(CharsetSuffixes.MixedLong),
_ => "Custom"
};
}
private static List Translate(Summary summary, List allTestData)
{
var grouped = summary.Reports
.Where(r => r.Success && r.ResultStatistics != null)
.GroupBy(r => (
TestData: GetParam(r, "TestData"),
Engine: GetParam(r, "Engine")
))
.Where(g => !string.IsNullOrEmpty(g.Key.TestData) && !string.IsNullOrEmpty(g.Key.Engine))
.ToList();
var results = new List(grouped.Count);
foreach (var group in grouped)
{
var testDataSet = (TestDataSet)allTestData.First(t => t.Name.StartsWith(group.Key.TestData));
var engineEnum = group.Key.Engine switch
{
"AcBinary" => BenchmarkEngine.AcBinary,
"MemoryPack" => BenchmarkEngine.MemoryPack,
_ => throw new InvalidOperationException($"Unknown engine in BDN params: {group.Key.Engine}")
};
// Construct the same workload instance AcBinaryVsMemPackBenchmark.Setup would build — same options,
// same wire mode. Reading SerializedSize + OptionsDescription from it keeps the BDN-side metadata
// in lockstep with what the workload actually serialised (no drift between hardcoded BDN strings
// and the workload's own OptionsDescription / SerializedSize).
var workload = CreateWorkload(testDataSet, group.Key.Engine);
var result = new BenchmarkResult
{
TestDataName = testDataSet.DisplayName,
Engine = engineEnum,
IoMode = BenchmarkIoMode.ByteArray,
DispatchMode = BenchmarkDispatchMode.SGen,
OptionsPreset = group.Key.Engine == "AcBinary" ? "FastMode" : "Default",
OrderTypeName = nameof(TestOrder_All_False),
SerializedSize = workload.SerializedSize,
OptionsDescription = workload.OptionsDescription,
};
// ns → ms (BenchmarkResult expects ms per op with iter=1, so µs/op = ms * 1000 / 1 = ms*1000).
const double nsToMs = 1.0 / 1_000_000.0;
foreach (var report in group)
{
var methodName = report.BenchmarkCase.Descriptor.WorkloadMethod.Name;
var stats = report.ResultStatistics!;
var allocBytes = report.GcStats.GetBytesAllocatedPerOperation(report.BenchmarkCase) ?? 0;
if (methodName == "Serialize")
{
result.SerializeTimeMs = stats.Median * nsToMs;
result.SerializeTimeMinMs = stats.Min * nsToMs;
result.SerializeTimeMaxMs = stats.Max * nsToMs;
result.SerializeTimeStdDevMs = stats.StandardDeviation * nsToMs;
result.SerializeIterations = 1; // see class-doc "Iteration count = 1" note
result.SerializeAllocBytesPerOp = allocBytes;
}
else if (methodName == "Deserialize")
{
result.DeserializeTimeMs = stats.Median * nsToMs;
result.DeserializeTimeMinMs = stats.Min * nsToMs;
result.DeserializeTimeMaxMs = stats.Max * nsToMs;
result.DeserializeTimeStdDevMs = stats.StandardDeviation * nsToMs;
result.DeserializeIterations = 1;
result.DeserializeAllocBytesPerOp = allocBytes;
}
}
// Compose RT from Ser + Des per-op µs (same logic as Console BenchmarkLoop's in-memory
// composition — since BDN measures Ser and Des independently, RT here is the analytic sum).
var serPerOp = BenchmarkReportWriter.ToPerOpMicros(result.SerializeTimeMs, result.SerializeIterations);
var desPerOp = BenchmarkReportWriter.ToPerOpMicros(result.DeserializeTimeMs, result.DeserializeIterations);
var rtPerOp = serPerOp + desPerOp;
result.RoundTripIterations = Math.Max(result.SerializeIterations, result.DeserializeIterations);
result.RoundTripTimeMs = rtPerOp / 1000.0 * result.RoundTripIterations;
result.RoundTripAllocBytesPerOp = result.SerializeAllocBytesPerOp + result.DeserializeAllocBytesPerOp;
results.Add(result);
}
return results;
}
private static string GetParam(BenchmarkReport report, string name) =>
report.BenchmarkCase.Parameters.Items.FirstOrDefault(p => p.Name == name)?.Value?.ToString() ?? "";
///
/// Constructs the same workload instance would build —
/// same options, same wire mode. The adapter reads and
/// from this instance so the BDN-side BenchmarkResult
/// rows carry the same workload-side metadata the Console rows have (no risk of drift between hardcoded
/// adapter strings and what the workload actually used).
///
/// Cost: one Serialize call inside the ctor per (TestData × Engine) cell — runs once during summary
/// translation, NOT in BDN's measured hot path. Negligible vs BDN's per-run cost.
///
private static ISerializerBenchmark CreateWorkload(TestDataSet testDataSet, string engine)
{
if (engine == "AcBinary")
{
var options = AcBinarySerializerOptions.FastMode;
options.WireMode = WireMode.Compact;
return new AcBinaryBenchmark(testDataSet.Order, options, "FastMode");
}
return new MemoryPackBenchmark(testDataSet.Order, WireMode.Compact, "Default");
}
}