diff --git a/AyCode.Core.Serializers.Console/Output.cs b/AyCode.Benchmark/Reporting/BenchmarkReportWriter.cs
similarity index 88%
rename from AyCode.Core.Serializers.Console/Output.cs
rename to AyCode.Benchmark/Reporting/BenchmarkReportWriter.cs
index 04ac97b..98248db 100644
--- a/AyCode.Core.Serializers.Console/Output.cs
+++ b/AyCode.Benchmark/Reporting/BenchmarkReportWriter.cs
@@ -1,4 +1,3 @@
-using AyCode.Core.Benchmarks.Reporting;
using AyCode.Core.Benchmarks.Workloads.Scenarios;
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Tests.TestModels;
@@ -6,15 +5,20 @@ using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
-namespace AyCode.Core.Serializers.Console;
+namespace AyCode.Core.Benchmarks.Reporting;
///
-/// Output formatters for the benchmark suite: per-row console table, .log file, .LLM markdown file.
-/// Consumes rows produced by the benchmark loop and emits human-readable
-/// + LLM-friendly outputs. Display-only helpers (per-op µs conversion, allocation KB formatting,
-/// range/CV formatting, paired-aggregation stats) live here too — they are display-side concerns.
+/// Output formatters for the benchmark suite: per-row console table, .log file, .LLM markdown file, .output
+/// binary hex dump. Consumes rows produced by the benchmark execution loop
+/// (Console-side BenchmarkLoop or BDN-side BdnSummaryAdapter) and emits human-readable +
+/// LLM-friendly outputs.
+///
+/// The parameter encapsulates per-run state —
+/// drives the filename prefix ("Console" / "Bdn"), is the
+/// resolved (walk-up-to-.sln) output folder, and the remaining fields (charset, iter counts, target sample
+/// window, CV threshold) carry the run-header info embedded in every emitted artifact.
///
-internal static class Output
+public static class BenchmarkReportWriter
{
///
/// Per-cell-paired aggregation of an overall comparison. Captures three different aggregation
@@ -27,18 +31,18 @@ internal static class Output
/// Arithmetic mean AcBinary value (µs/op or bytes).
/// Arithmetic mean MemPack value.
/// Number of paired cells contributing to the geo/median.
- internal record OverallStats(double ArithMeanPct, double GeoMeanPct, double MedianPct, double AcAvg, double MpAvg, int CellCount);
+ public record OverallStats(double ArithMeanPct, double GeoMeanPct, double MedianPct, double AcAvg, double MpAvg, int CellCount);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static double ToPerOpMicros(double totalMs, int iterations) => iterations > 0 ? totalMs / iterations * 1000.0 : 0;
+ public static double ToPerOpMicros(double totalMs, int iterations) => iterations > 0 ? totalMs / iterations * 1000.0 : 0;
// Per-row per-op µs accessors — pull batch-time + iter from BenchmarkResult and convert. Used wherever
// averaging or comparison happens across rows with potentially different iter counts (Winners summary,
// Overall comparison, per-cell summary row). Keeping these as methods rather than properties on
// BenchmarkResult preserves the result-as-data-bag distinction.
- internal static double SerPerOp(BenchmarkResult r) => ToPerOpMicros(r.SerializeTimeMs, r.SerializeIterations);
- internal static double DesPerOp(BenchmarkResult r) => ToPerOpMicros(r.DeserializeTimeMs, r.DeserializeIterations);
- internal static double RtPerOp(BenchmarkResult r) => ToPerOpMicros(r.RoundTripTimeMs, r.RoundTripIterations);
+ public static double SerPerOp(BenchmarkResult r) => ToPerOpMicros(r.SerializeTimeMs, r.SerializeIterations);
+ public static double DesPerOp(BenchmarkResult r) => ToPerOpMicros(r.DeserializeTimeMs, r.DeserializeIterations);
+ public static double RtPerOp(BenchmarkResult r) => ToPerOpMicros(r.RoundTripTimeMs, r.RoundTripIterations);
///
/// Converts a byte count to KB (1 KB = 1024 B). Display-only helper so allocation columns can
@@ -47,7 +51,7 @@ internal static class Output
/// integers untouched.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static double ToKilobytes(long bytes) => bytes / 1024.0;
+ public static double ToKilobytes(long bytes) => bytes / 1024.0;
///
/// Computes arithmetic + geometric + median aggregation of an AcBinary-vs-MemPack comparison
@@ -55,7 +59,7 @@ internal static class Output
/// geo/median variants — a cell where AcBinary or MemPack is missing is dropped from all stats.
/// Returns null when no paired cell has a valid value.
///
- internal static OverallStats? ComputeOverallStats(List acResults, List mpResults, Func getValue)
+ public static OverallStats? ComputeOverallStats(List acResults, List mpResults, Func getValue)
{
if (acResults.Count == 0 || mpResults.Count == 0) return null;
@@ -93,12 +97,12 @@ internal static class Output
///
/// Formats a per-op micros value with its inter-sample range and CV-threshold marker as
/// "26.86 (24.5..29.1)" or "26.86 (24.5..29.1) ⚠️5.2%". Median first, range in parentheses,
- /// CV warning suffix only when CV > . When min == max == median
+ /// CV warning suffix only when CV > . When min == max == median
/// (single-sample / Debug / quick mode), collapses to bare median to avoid visual clutter.
/// All time inputs are total-batch milliseconds; is the per-row iter
/// count (post-adaptive-calibration).
///
- internal static string FormatMicrosWithRange(double medianMs, double minMs, double maxMs, double stdDevMs, int iterations, CultureInfo inv)
+ public static string FormatMicrosWithRange(double medianMs, double minMs, double maxMs, double stdDevMs, int iterations, CultureInfo inv, double unstableCvThreshold)
{
var med = ToPerOpMicros(medianMs, iterations);
// No range data (single-sample fast path) — surface as bare median, identical to the prior format.
@@ -114,7 +118,7 @@ internal static class Output
if (medianMs > 0 && stdDevMs > 0)
{
var cv = stdDevMs / medianMs;
- if (cv > Configuration.UnstableCVThreshold)
+ if (cv > unstableCvThreshold)
{
var cvPct = (cv * 100).ToString("F1", inv);
return $"{range} ⚠️{cvPct}%";
@@ -128,14 +132,14 @@ internal static class Output
/// Formats a signed percent delta with explicit sign for positive values (`+1.5%`, `-3.0%`, `0.0%`).
/// Padded to 7 chars (e.g. ` +12.3%`, `-100.0%`) for column alignment in the Overall block.
///
- internal static string FormatPctSigned(double pct) => pct.ToString("+0.0;-0.0;0.0", CultureInfo.InvariantCulture).PadLeft(6) + "%";
+ public static string FormatPctSigned(double pct) => pct.ToString("+0.0;-0.0;0.0", CultureInfo.InvariantCulture).PadLeft(6) + "%";
///
/// Renders one Overall row with arith / geo / median deltas + AcBinary/MemPack absolute means.
/// Color is driven by the geometric-mean delta (magnitude-neutral signal). Skips silently when
/// stats is null (no paired data).
///
- internal static void WriteOverallLine(string label, string unit, OverallStats? stats, string fmt = "F2")
+ public static void WriteOverallLine(string label, string unit, OverallStats? stats, string fmt = "F2")
{
if (stats == null) return;
@@ -150,13 +154,13 @@ internal static class Output
/// Same as but appends to a (no color).
/// Used by the .log and .LLM file writers.
///
- internal static void AppendOverallLine(StringBuilder sb, string label, string unit, OverallStats? stats, string fmt = "F2")
+ public static void AppendOverallLine(StringBuilder sb, string label, string unit, OverallStats? stats, string fmt = "F2")
{
if (stats == null) return;
sb.AppendLine($" {label,-12} arith {FormatPctSigned(stats.ArithMeanPct)} | geo {FormatPctSigned(stats.GeoMeanPct)} | median {FormatPctSigned(stats.MedianPct)} ({stats.AcAvg.ToString(fmt, CultureInfo.InvariantCulture)} {unit} vs {stats.MpAvg.ToString(fmt, CultureInfo.InvariantCulture)} {unit}, {stats.CellCount} cells)");
}
- internal static void PrintResult(BenchmarkResult result)
+ public static void PrintResult(BenchmarkResult result)
{
// Numbers-only per-row entries; the column-headers carry units (µs/op, KB/op).
var ser = result.SerializeTimeMs > 0 ? $"{SerPerOp(result),7:F2}" : " N/A";
@@ -166,7 +170,7 @@ internal static class Output
System.Console.WriteLine($" {result.SerializerName,-40} | Size: {result.SerializedSize,8:N0} B | Ser: {ser} µs/op ({serAlloc} KB/op) | Des: {des} µs/op ({desAlloc} KB/op)");
}
- internal static void PrintGroupedResults(List results, List testDataSets)
+ public static void PrintGroupedResults(List results, List testDataSets)
{
System.Console.WriteLine("\n");
System.Console.WriteLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗");
@@ -407,14 +411,21 @@ internal static class Output
WriteOverallLine("Des Alloc", "B/op", desAllocStats, "F0");
}
- internal static void SaveResults(List results, List testDataSets)
+ ///
+ /// Writes the unified file triplet — {SourceTag}.FullBenchmark_{Build}_{timestamp}.{log, LLM, output}
+ /// — to . The .log is the human-readable formatted
+ /// view, the .LLM is the markdown LLM-paste-friendly view, and the .output is a binary hex
+ /// dump of the Large test data's AcBinary-Default serialization (for raw inspection / wire-debugging).
+ ///
+ public static void SaveAll(ReportingContext ctx, List results, List testDataSets)
{
- Directory.CreateDirectory(Configuration.ResultsDirectory);
+ Directory.CreateDirectory(ctx.ResultsDirectory);
var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
- var baseFileName = $"Console.FullBenchmark_{Configuration.BuildConfiguration}_{timestamp}";
- var logFilePath = Path.Combine(Configuration.ResultsDirectory, $"{baseFileName}.log");
- var outputFilePath = Path.Combine(Configuration.ResultsDirectory, $"{baseFileName}.output");
+ var baseFileName = $"{ctx.SourceTag}.FullBenchmark_{ctx.BuildConfiguration}_{timestamp}";
+ var logFilePath = Path.Combine(ctx.ResultsDirectory, $"{baseFileName}.log");
+ var outputFilePath = Path.Combine(ctx.ResultsDirectory, $"{baseFileName}.output");
+ var llmFilePath = Path.Combine(ctx.ResultsDirectory, $"{baseFileName}.LLM");
// Save binary output to separate .output file.
// Cast to TestDataSet because Phase 1 hardcodes the benchmark variant.
@@ -436,7 +447,7 @@ internal static class Output
outputSb.AppendLine("Hex dump:");
outputSb.AppendLine(FormatHexDump(serializedBytes));
- File.WriteAllText(outputFilePath, outputSb.ToString(), Configuration.Utf8NoBom);
+ File.WriteAllText(outputFilePath, outputSb.ToString(), ctx.Utf8NoBom);
System.Console.WriteLine($"✓ Binary output saved to: {outputFilePath}");
}
@@ -445,10 +456,11 @@ internal static class Output
sb.AppendLine("╔══════════════════════════════════════════════════════════════════════════════════════════════════════╗");
sb.AppendLine("║ SERIALIZER BENCHMARK RESULTS ║");
sb.AppendLine($"║ Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}".PadRight(100) + "║");
- sb.AppendLine($"║ Build: {Configuration.BuildConfiguration}".PadRight(100) + "║");
- sb.AppendLine($"║ Charset: {Configuration.GetCurrentCharsetName()}".PadRight(100) + "║");
- sb.AppendLine($"║ Iterations: per-cell adaptive (~{Configuration.TargetSampleMs} ms target)".PadRight(100) + "║");
- sb.AppendLine($"║ Samples: {Configuration.BenchmarkSamples} (median) + 1 pilot discarded".PadRight(100) + "║");
+ sb.AppendLine($"║ Source: {ctx.SourceTag}".PadRight(100) + "║");
+ sb.AppendLine($"║ Build: {ctx.BuildConfiguration}".PadRight(100) + "║");
+ sb.AppendLine($"║ Charset: {ctx.CharsetName}".PadRight(100) + "║");
+ sb.AppendLine($"║ Iterations: per-cell adaptive (~{ctx.TargetSampleMs} ms target)".PadRight(100) + "║");
+ sb.AppendLine($"║ Samples: {ctx.BenchmarkSamples} (median) + 1 pilot discarded".PadRight(100) + "║");
sb.AppendLine("╚══════════════════════════════════════════════════════════════════════════════════════════════════════╝");
sb.AppendLine();
@@ -471,6 +483,10 @@ internal static class Output
}
// CSV-like data for easy import — keeps raw byte integers (no KB rounding) so external tools can compute precisely.
+ // InvariantCulture is mandatory here: the decimal-separator dimension MUST be `.` so the comma-separated
+ // field delimiters don't collide with locale-specific decimal commas (e.g. Hungarian "7,38" would split
+ // a single F2 value across two CSV fields).
+ var inv = CultureInfo.InvariantCulture;
sb.AppendLine("=== RAW DATA (CSV) ===");
sb.AppendLine("TestData,Engine,IO,Mode,Options,Size,SerializeMicrosPerOp,DeserializeMicrosPerOp,RoundTripMicrosPerOp,SerializeAllocBytesPerOp,DeserializeAllocBytesPerOp,RoundTripAllocBytesPerOp,SetupSerializeAllocBytes,SetupDeserializeAllocBytes");
@@ -479,7 +495,7 @@ internal static class Output
var testResults = results.Where(r => r.TestDataName == testData.DisplayName).ToList();
foreach (var result in testResults)
{
- sb.AppendLine($"{result.TestDataName},{result.Engine.ToDisplay()},{result.IoMode.ToDisplay()},{result.DispatchMode.ToDisplay()},{result.OptionsPreset},{result.SerializedSize},{SerPerOp(result):F2},{DesPerOp(result):F2},{RtPerOp(result):F2},{result.SerializeAllocBytesPerOp},{result.DeserializeAllocBytesPerOp},{result.RoundTripAllocBytesPerOp},{result.SetupSerializeAllocBytes},{result.SetupDeserializeAllocBytes}");
+ sb.AppendLine($"{result.TestDataName},{result.Engine.ToDisplay()},{result.IoMode.ToDisplay()},{result.DispatchMode.ToDisplay()},{result.OptionsPreset},{result.SerializedSize},{SerPerOp(result).ToString("F2", inv)},{DesPerOp(result).ToString("F2", inv)},{RtPerOp(result).ToString("F2", inv)},{result.SerializeAllocBytesPerOp},{result.DeserializeAllocBytesPerOp},{result.RoundTripAllocBytesPerOp},{result.SetupSerializeAllocBytes},{result.SetupDeserializeAllocBytes}");
}
}
sb.AppendLine();
@@ -546,11 +562,10 @@ internal static class Output
if (memPackRtResults2.Count == 0 || acBinaryRtResults2.Count == 0)
{
sb.AppendLine(" (Comparison requires both serialize and deserialize data)");
- File.WriteAllText(logFilePath, sb.ToString(), Configuration.Utf8NoBom);
+ File.WriteAllText(logFilePath, sb.ToString(), ctx.Utf8NoBom);
System.Console.WriteLine($"✓ Results saved to: {logFilePath}");
- var llmFilePathEarly = Path.Combine(Configuration.ResultsDirectory, $"{baseFileName}.LLM");
- SaveLlmResults(llmFilePathEarly, results, testDataSets);
+ SaveLlmResults(ctx, llmFilePath, results, testDataSets);
return;
}
@@ -564,23 +579,22 @@ internal static class Output
AppendOverallLine(sb, "Round-trip", "µs/op", ComputeOverallStats(acBinaryRtResults2, memPackRtResults2, RtPerOp));
AppendOverallLine(sb, "Size", "B", ComputeOverallStats(sizeAcResults2, sizeMpResults2, r => r.SerializedSize), "F0");
- File.WriteAllText(logFilePath, sb.ToString(), Configuration.Utf8NoBom);
+ File.WriteAllText(logFilePath, sb.ToString(), ctx.Utf8NoBom);
System.Console.WriteLine($"✓ Results saved to: {logFilePath}");
// Save LLM-optimized results
- var llmFilePath = Path.Combine(Configuration.ResultsDirectory, $"{baseFileName}.LLM");
- SaveLlmResults(llmFilePath, results, testDataSets);
+ SaveLlmResults(ctx, llmFilePath, results, testDataSets);
}
- internal static void SaveLlmResults(string filePath, List results, List testDataSets)
+ private static void SaveLlmResults(ReportingContext ctx, string filePath, List results, List testDataSets)
{
var sb = new StringBuilder();
- sb.AppendLine($"# AcBinary Benchmark {Configuration.BuildConfiguration} {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
- sb.AppendLine($"Charset: {Configuration.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} | UnstableCV threshold: {Configuration.UnstableCVThreshold * 100:F0}%");
+ sb.AppendLine($"# AcBinary Benchmark [{ctx.SourceTag}] {ctx.BuildConfiguration} {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
+ sb.AppendLine($"Charset: {ctx.CharsetName} | Iterations: per-cell adaptive (target ~{ctx.TargetSampleMs} ms/sample) | Warmup: {ctx.WarmupIterations} per phase (Ser/Des isolated) | Samples: {ctx.BenchmarkSamples} (median) + 1 pilot discarded | .NET: {Environment.Version} | UnstableCV threshold: {ctx.UnstableCVThreshold * 100:F0}%");
sb.AppendLine("Baseline: MemoryPack (Byte[]) (SOTA reference) | Verified: round-trip correctness checked once per cell before warmup");
// Options summary. Bracketed [OrderType] surfaces the TestOrder variant each preset serialised —
- // see SaveResults for the variant-dispatch rationale.
+ // see SaveAll for the variant-dispatch rationale.
var optionsMap = results
.Where(r => r.OptionsDescription != null)
.Select(r => (r.SerializerName, r.OrderTypeName, r.OptionsDescription!))
@@ -601,6 +615,8 @@ internal static class Output
sb.AppendLine("TestData | Engine | IO | Mode | Options | Size(B) | Ser(µs/op) | Deser(µs/op) | RT(µs/op) | SerAlloc(KB/op) | DesAlloc(KB/op) | RTAlloc(KB/op) | Setup S/D(KB) | Iter Ser/Des");
sb.AppendLine("---|---|---|---|---|---|---|---|---|---|---|---|---|---");
+ var inv = CultureInfo.InvariantCulture;
+
foreach (var testData in testDataSets)
{
var testResults = results
@@ -610,12 +626,11 @@ internal static class Output
foreach (var r in testResults)
{
- var inv = CultureInfo.InvariantCulture;
- var ser = r.SerializeTimeMs > 0 ? FormatMicrosWithRange(r.SerializeTimeMs, r.SerializeTimeMinMs, r.SerializeTimeMaxMs, r.SerializeTimeStdDevMs, r.SerializeIterations, inv) : "-";
- var des = r.DeserializeTimeMs > 0 ? FormatMicrosWithRange(r.DeserializeTimeMs, r.DeserializeTimeMinMs, r.DeserializeTimeMaxMs, r.DeserializeTimeStdDevMs, r.DeserializeIterations, inv) : "-";
+ var ser = r.SerializeTimeMs > 0 ? FormatMicrosWithRange(r.SerializeTimeMs, r.SerializeTimeMinMs, r.SerializeTimeMaxMs, r.SerializeTimeStdDevMs, r.SerializeIterations, inv, ctx.UnstableCVThreshold) : "-";
+ var des = r.DeserializeTimeMs > 0 ? FormatMicrosWithRange(r.DeserializeTimeMs, r.DeserializeTimeMinMs, r.DeserializeTimeMaxMs, r.DeserializeTimeStdDevMs, r.DeserializeIterations, inv, ctx.UnstableCVThreshold) : "-";
var rt = r.RoundTripTimeMs > 0
? (r.IsRoundTripOnly
- ? FormatMicrosWithRange(r.RoundTripTimeMs, r.RoundTripTimeMinMs, r.RoundTripTimeMaxMs, r.RoundTripTimeStdDevMs, r.RoundTripIterations, inv)
+ ? FormatMicrosWithRange(r.RoundTripTimeMs, r.RoundTripTimeMinMs, r.RoundTripTimeMaxMs, r.RoundTripTimeStdDevMs, r.RoundTripIterations, inv, ctx.UnstableCVThreshold)
: RtPerOp(r).ToString("F2", inv))
: "-";
@@ -658,14 +673,14 @@ internal static class Output
sb.AppendLine("```");
}
- File.WriteAllText(filePath, sb.ToString(), Configuration.Utf8NoBom);
+ File.WriteAllText(filePath, sb.ToString(), ctx.Utf8NoBom);
System.Console.WriteLine($"✓ LLM results saved to: {filePath}");
}
///
/// Formats byte array as hex dump with offset, hex values, and ASCII representation.
///
- internal static string FormatHexDump(byte[] bytes, int bytesPerLine = 16)
+ public static string FormatHexDump(byte[] bytes, int bytesPerLine = 16)
{
var sb = new StringBuilder();
for (var i = 0; i < bytes.Length; i += bytesPerLine)
diff --git a/AyCode.Benchmark/Reporting/ReportingContext.cs b/AyCode.Benchmark/Reporting/ReportingContext.cs
index 566e01d..666130a 100644
--- a/AyCode.Benchmark/Reporting/ReportingContext.cs
+++ b/AyCode.Benchmark/Reporting/ReportingContext.cs
@@ -4,37 +4,34 @@ namespace AyCode.Core.Benchmarks.Reporting;
///
/// Context bundle for the unified benchmark report writer. Same record on both sides (Console / BDN),
-/// only the differs ("Console" / "Bdn") — that drives the filename prefix
+/// the differs ("Console" / "Bdn") and drives the filename prefix
/// (e.g. Console.FullBenchmark_Release_{timestamp}.LLM vs Bdn.FullBenchmark_Release_{timestamp}.LLM).
/// The resolution walks up from to the
/// nearest AyCode.Core.sln and combines with Test_Benchmark_Results\Benchmark — works across
/// build modes (Debug / Release / AOT publish) and worktrees (each worktree has its own .sln, so its bench
-/// results land alongside its code).
+/// results land alongside its code). The remaining fields capture run-header information (charset, iter
+/// counts, target sample window, CV threshold) so the writer can render a self-documenting header in both
+/// the .log and .LLM outputs.
///
public sealed record ReportingContext(
string SourceTag,
string ResultsDirectory,
string BuildConfiguration,
- UTF8Encoding Utf8NoBom)
+ UTF8Encoding Utf8NoBom,
+ string CharsetName,
+ int WarmupIterations,
+ int BenchmarkSamples,
+ int TargetSampleMs,
+ double UnstableCVThreshold)
{
///
- /// Resolves the canonical by walking up from
- /// to the nearest AyCode.Core.sln, then combining
- /// with Test_Benchmark_Results\Benchmark. The build configuration is supplied by the caller
- /// (Console resolves via #if AYCODE_NATIVEAOT|DEBUG|else Release; BDN-side currently mirrors
- /// the JIT vs AOT discriminator from ).
- ///
- public static ReportingContext Create(string sourceTag, string buildConfiguration) =>
- new(sourceTag, ResolveResultsDirectory(), buildConfiguration, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
-
- ///
- /// Walk-up from the assembly's BaseDirectory to find the repo root (marker: AyCode.Core.sln).
+ /// Walks up from the assembly's BaseDirectory to find the repo root (marker: AyCode.Core.sln).
/// Returns {repoRoot}\Test_Benchmark_Results\Benchmark. Worktree-aware: if running from a
/// worktree, the walk finds the worktree's own .sln (each worktree has its own checkout), so
/// results land in the worktree's results folder — the natural place when the worktree's code
/// changes are what produced the numbers.
///
- private static string ResolveResultsDirectory()
+ public static string ResolveResultsDirectory()
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir != null && !File.Exists(Path.Combine(dir.FullName, "AyCode.Core.sln")))
diff --git a/AyCode.Core.Serializers.Console/BenchmarkLoop.cs b/AyCode.Core.Serializers.Console/BenchmarkLoop.cs
index 33d7ad7..12a331a 100644
--- a/AyCode.Core.Serializers.Console/BenchmarkLoop.cs
+++ b/AyCode.Core.Serializers.Console/BenchmarkLoop.cs
@@ -127,11 +127,23 @@ internal static class BenchmarkLoop
allResults.AddRange(results);
}
- // Print grouped results
- Output.PrintGroupedResults(allResults, testDataSets);
+ // Build the reporting context (resolves path via walk-up to .sln, snapshots run-config).
+ var ctx = new ReportingContext(
+ SourceTag: "Console",
+ ResultsDirectory: ReportingContext.ResolveResultsDirectory(),
+ BuildConfiguration: Configuration.BuildConfiguration,
+ Utf8NoBom: Configuration.Utf8NoBom,
+ CharsetName: Configuration.GetCurrentCharsetName(),
+ WarmupIterations: Configuration.WarmupIterations,
+ BenchmarkSamples: Configuration.BenchmarkSamples,
+ TargetSampleMs: Configuration.TargetSampleMs,
+ UnstableCVThreshold: Configuration.UnstableCVThreshold);
- // Save results to file
- Output.SaveResults(allResults, testDataSets);
+ // Print grouped results
+ BenchmarkReportWriter.PrintGroupedResults(allResults, testDataSets);
+
+ // Save results to file (.log + .LLM + .output)
+ BenchmarkReportWriter.SaveAll(ctx, allResults, testDataSets);
System.Console.WriteLine("\n✓ Benchmark complete!");
}
@@ -275,8 +287,8 @@ internal static class BenchmarkLoop
// batch-time addition would be misleading. Instead: compute per-op µs (iter-independent),
// then synthesize RoundTripTimeMs against RoundTripIterations = max(serIter, desIter) so that
// RoundTripTimeMs / RoundTripIterations * 1000 == Output.SerPerOp + Output.DesPerOp.
- var serPerOp = Output.ToPerOpMicros(result.SerializeTimeMs, result.SerializeIterations);
- var desPerOp = Output.ToPerOpMicros(result.DeserializeTimeMs, result.DeserializeIterations);
+ 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;
@@ -284,7 +296,7 @@ internal static class BenchmarkLoop
}
results.Add(result);
- Output.PrintResult(result);
+ BenchmarkReportWriter.PrintResult(result);
}
// Dispose any IDisposable serializers (NamedPipe / FileStream variants own OS resources that must be released