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, MicroOptCVThreshold: 0.015); } /// /// 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"); } }